4.0 Operations Prologue
In this chapter we focus on ways that C++ data operations are implemented and tested, e.g.,
functions, methods, lambdas and more. The content applies to almost everything
you implement with C++. Getting familiar with these topics will make it easier to digest discussions of classes and templates
in succeeding chapters.
Quick Starter Example - Function Dispatching & Callbacks
This example demonstrates that C++ functions can accept and return other functions.
You may be puzzeled by the first line of code - that's a function pointer declaration.
We will discuss those soon in this chapter.
Code: function dispatching & callbacks
/*-- define function dispatcher --*/
using FPtr = void(*)();
void fun1() {
std::cout << "\n index == 1 => fun1 called";
}
void fun2() {
std::cout << "\n index == 2 => fun2 called";
}
void fun3() {
std::cout << "\n index == 3 => fun3 called";
}
void oophs() {
std::string msg = "other index => ";
msg += "can't find that function";
std::cout << "\n " << msg;
}
auto functionDispatcher(size_t index) {
switch (index) {
case 1:
return &fun1;
break;
case 2:
return &fun2;
break;
case 3:
return &fun3;
break;
default:
return &oophs;
}
}
/*-- define callback --*/
void applicationSpecificCallback() {
std::string msg = "Pretending to cleanup ";
msg += "at end of function";
std::cout << "\n " << msg;
}
auto functionWithCallback(FPtr callback) {
std::string msg = "Pretending to do some ";
msg += "standard function";
std::cout << "\n " << msg;
callback();
}
Using Code
int main() {
/* must use C++17 option to compile */
displayDemo(
"-- function dispatcher --\n"
);
functionDispatcher(1)();
functionDispatcher(2)();
functionDispatcher(3)();
functionDispatcher(4)();
displayDemo(
"\n -- function with callback --"
);
functionWithCallback(
applicationSpecificCallback
);
std::cout << "\n\n";
}
Output
-- function dispatcher --
index == 1 => fun1 called
index == 2 => fun2 called
index == 3 => fun3 called
other index => can't find that function
-- function with callback --
pretending to do some standard function
pretending to cleanup at end of function
Passing and return functions - o.k. function pointers - is a powerful way to make
your designs flexible.
Conclusion:
Function callbacks and dispatch are tools most professional developers use fairly often.
This example dispatches functions and makes callbacks using function pointers. By substituting
those with lambdas in the function arguments and return values - very easy to do - we move closer to
functional programming.
Lambdas can accept and return other lambdas so they are higher-order callable objects.
Lambdas are,
by default, const. Their state can only be changed if they are qualified as mutable. If we don't
accept arguments and capture values by reference, then they have no side effects. Another characteristic
of functional programming.
4.1 Type Coercions
The fundamental C++ types support coercions between selected types.
Type Coercion
Coercion is the conversion of the representation of a value in one type
into the representation of the value in another type. For example when the
statements below are executed:
int i{ 5 };
double d = i;
the value of i, e.g., 5, is converted to the corresponding double precision
representation with sign bit, exponent, and fractional part.
For the fundamental types, any time the conversion source has the same or smaller size than
the destination, the conversion will succeed, silently. These are called numeric promotions or widening.
If the conversion has source type larger than destination type the conversion may or may not
succeed. These are called numeric conversions or narrowing.
Here are some examples of numeric promotions:
long int li{ 5 };
int i = short int{ 2 };
long long int {2L}
double d{ 3.0F };
and here are examples of numeric conversions:
int j = 2.5; // succeeds with warning, truncates 2.5 to 2
int j{ 2.5 }; // compile failure (see initialization section of Chapter 2)
float f = 1.5; // succeeds with warning
The suffixes L and F denote long int and float.
Suffix LL denotes a long long int.
If there are no suffixes an integral number has the type int and a floating point
number has the type double.
Coercions can be implemented for user-defined types using promotion constructors
and cast operators. We will cover that in detail in Chapter 5 - Classes.
4.2 Copy Construction Operations
Copy construction creates a new instance of some specified type and initializes with the state of an existing instance of the
same type. Here, we show copy construction syntax for double, a fundamental type, and struct.
Copy construction:
double d1 { 3.15149 };
double d2 { d1 }; // C++14 copy construction syntax - preferred
double d2 = { d1 }; // alternate C++14 copy construction syntax - not assignment
double d2 = d1; // C++98 copy construction syntax - not assignment
double d2(d1); // alternate C++98 copy construction syntax
Copy construction of structs
displayDemo("--- demo copy construct struct ---");
struct S { int i; double d; char c; int iArr[3]; };
S s1{ 1, 1.0 / 3.0, 'Q', { 1, 2, 3 } };
S s2{ s1 }; // copy construction
showStruct(s1, "src");
showStruct(s2, "cpy");
Output:
src struct: { 1, 0.333333, Q, [ 1 2 3 ] }
cpy struct: { 1, 0.333333, Q, [ 1 2 3 ] }
Note that for structs and classes, if no copy constructor is defined the compiler will generate one
that does member-wise copy operations. It is a pleasant suprise that works for arrays as well as the fundamental
types illustrated. I suspect that the compiler does a memcpy on the contiguous array memory to affect that copy.
Compiler generated operations will be discussed in detail in Chapter #5 - Classes.
4.3 Copy Assignment Operations
Copy assignment copies the state from a source instance of some specified type to the state of an existing instance of the
same type. Here, we show copy assignment syntax for std::byte, a fundamental type, and struct.
Copy assignment:
std::byte b1{ 0xf }; // value of b1 is 0xf
std::byte b2{ 0xe }; // value of b2 is 0xe
b2 = b1; // copy assignment - value of b2 is now 0xf
Copy assignment of structs
struct S {
int i = 1; double d = 1.0 / 3.0; char c = 'Q'; int iArr[3]{ 1,2,3 };
} s1;
S s2{ 2, 1.5, 'a', { 3, 2, 1 } };
showStruct(s1, "s1");
showStruct(s2, "s2");
std::cout << "\n after assignment s2 has values: ";
s2 = s1;
showStruct(s2, "s2");
Output:
s1 struct: { 1, 0.333333, Q, [ 1 2 3 ] }
s2 struct: { 2, 1.5, a, [ 3 2 1 ] }
after assignment s2 has values:
s2 struct: { 1, 0.333333, Q, [ 1 2 3 ] }
Note that for structs and classes, if no copy assignment operator is defined the compiler will generate one
that does member-wise copy assignment operations.
Compiler generated operations will be discussed in detail in Chapter #5 - Classes.
4.4 Functions
Functions have a name, a sequence of zero or more arguments, and a return type or void return,
wrapped around a block of code.
void putline(size_t n = 1) {
for(size_t i = 0; i < n; ++i)
std::cout << "\n";
}
They may have default arguments, as shown in this example, so calling putline()
results in a single new line being pushed into the terminal stream.
Function arguments may be passed by value, as shown above, or by reference
as shown in the code below.
void show(const std::vector<int>& vInt) {
for(int item : vInt)
std::cout << item << " ";
}
The & before vInt indicates that the vector argument is passed by
reference. To avoid side effects, we pass reference arguments as const
references. On rare occasions we may choose to pass a non-const reference
so the function may change the argument in the caller's scope, but this
makes the code harder to understand, requiring the designer to understand
what the function does to the passed arguments. Furthermore, literals
can only be passed by value or by const reference.
functions can only be defined at namespace scope or class or struct scope. They cannot be
defined within function scope. That means that C++ does not support inner functions. When
defined in class or struct scope they are said to be methods of the class or struct.
For the remainder of this story, we use the term method for any function defined in class
or struct scope. All functions defined at namespace scope will simply be referred to
as functions. If there is a possible ambiguity, we will use the qualifier global or unbound
to indicate that they are not methods. So, putline is a global function.
The name of a function is just a pointer to its code.
Each function should have a single responsibility and be small and simple enough to make
it (relatively) easy to understand and test. A good default size is 50 lines of code - that
can fit on a single page, so we can quickly see all the parts.
One measure of complexity that I use is a count of all scopes within a function body.
That is simply 1 + the count of all the open braces "{" inside
the body1, but doesn't count the complexity of functions that are called. Those we can
think about some other time. I use a complexity measure, CM = 10 (scopes) as an upper limit for my own
code, and try to make most functions have CM <= 5.
4.5 Function Pointers
Function pointers are used to:
- create callbacks
- pass processing to platform API functions
-
modify the way library functions operate, e.g., qsort accepts a comparator function pointer
- pass processing to plug-in interfaces
These kinds of applications can be built without using function pointers, but when using existing
frameworks, it's likely that you will need them, especially with platform APIs.
Function pointers can bind to any function as long as its return type and parameter types
match the function pointer declaration.
using FP = void(*)(size_t n); // function pointer type
FP pL = putline;
pL(1) // push a newline to terminal - func ptrs don't honor default params
We show more about how that is done in the syntax details, below, for both specific
and generic function pointer declarations. It's a bit complicated, so
we've shown several cases.
function pointer syntax
Here's how you declare, define, and use specific function pointers:
For the function:
void putLine(size_t n = 1) {
for (size_t i = 0; i < n; ++i)
std::cout << "\n";
}
Define a function pointer for that specific signature:
using FP1 = void(*)(size_t n); // function pointer type
FP1 pl1 = putLine;
pl1(1); // push single newline to terminal
You can define a function pointer more directly using auto
auto& pl2 = putLine;
pl2(2); // push two newlines to the terminal
And here's how you make function pointers generic:
For the functions:
size_t size(const std::string& s) {
return s.size();
}
template <typename T>
void message(T t) {
std::cout << t;
}
}
Declare a generic function pointer type:
template<class Tr, Ta>
using FP2 = Tr(*)(Ta t);
and instantiate it for the second and third test functions:
FP2<size_t, const std::string&> pSz = size;
size_t sz = pSz("a test string");
FP2<void, const std::string&> msg1 = message;
msg1("\n a test message");
You can also define function pointers in a generic way using auto:
auto& msg2 = message<const std::string&>
msg2("\n another test message");
auto& msg3 = message<const char*>
msg3("\n still another test message!");
auto& pSz2 = size;
msg2(
"\n size of \"another, somewhat longer, string\" = " +
std::to_string(sz)
);
Function pointers are used in the Windows and Linux APIs, in the C Language libraries,
in Qt - a cross platform GUI framework, ...
4.6 Methods
Functions bound to classes and structs are called methods. Methods have access to all the
member data of the class. If a class inherits from one or more base classes, it inherits all
the methods and data members of its base classes, and has access to any of that qualified as
"public: or protected:" for methods and qualified as "protected:" for
data.
Base class
class B {
public:
void name(const std::string& aname) {
name_ = aname;
}
std::string name() {
return name_;
}
protected:
std::string name_;
};
Derived class
class D : public B {
public:
void occupation(const std::string& occup) {
occupation_ = occup;
}
std::string occupation() {
return ocupation_;
}
private:
std::string occupation_;
};
The class D, in the block above, has public methods:
-
void name(const std::string& aname)
-
std::string name()
-
void occupation(const std::string&; occup)
-
std::string occupation()
The two name methods it inherited from B and those are accessible to clients. It implemented
the two occupation methods and those are also accessible to clients.
The derived class D not only contains its occupation_ string, but it also contains the
Base::name_ string, inside an image of B that is part of its memory footprint. We will
demonstrate that in the next chapter - Classes.
4.7 Method Pointers
Method pointers have the same uses as function pointers. They have an advantage that
they can use member data from the invoking instance.
Method pointers can be bound to any method of the specified type provided that the
function arguments and return value match the method pointer declaraton.
using FP1 = void(D::B::*)(const std::string&);
FP1 pNameSetter = &D::B::name; // binds to void B::name(const std::string&)
D d;
(d.*pNameSetter)("Tom");
using FP2 = std::string(D::B::*)();
FP2 pNameGetter = &D::B::name; // binds to std::string B::name()
std::string name = (d.*pNameGetter)();
std::cout << "\n d1.name() --> " << name;
Note that binding to the inherited name functions requires using the class specifier
D::B instructing the compiler that it will find the code (to point to) in the base
class definition. When binding to the occupation names you will just use the D class
specifier. Look at the syntax details, below, to see all the details.
method pointer syntax
Here's how you declare, define, and use specific method pointers:
-- all the cases are included because syntax is a bit complex --
Using classes B and D defined above:
/* accessing inherited overloaded methods name */
using FP1 = void(D::B::*)(const std::string&);
FP1 pNameSetter = &D::B::name;
D d;
(d.*pNameSetter)("Tom");
using FP2 = std::string(D::B::*)();
FP2 pNameGetter = &D::B::name;
std::string name = (d.*pNameGetter)();
std::cout << "\n d1.name() --> " << name;
/* accessing overloaded methods occupation in D */
using FP3 = void(D::*)(const std::string&);
FP3 pOccupSetter = &D::occupation;
(d.*pOccupSetter)("derivatives analyst");
using FP4 = std::string(D::*)();
FP4 pOccupGetter = &D::occupation;
std::string myJob = (d.*pOccupGetter)();
std::cout << "\n d1.Occupation() -- > " << myJob;
/* std::invoke */
using std::invoke with a method pointer:
std::invoke(pNameSetter, d, "Darth Vader");
std::string darth = std::invoke(pNameGetter, d);
std::cout << "\n his name is " << darth;
/*-- alternate definitions --*/
auto pOccuSetter2 = static_cast<std::string(D::*)()>(&D::occupation);
auto pOccuGetter2 = static_cast<void(D::*)(const std::string&)>(&D::occupation);
std::string(D:: * pOccuGetter3)() = &D::occupation;
void(D:: * pOccuSetter3)(const std::string&) = &D::occupation;
Here's the output:
d1.name() --> Tom
d1.Occupation() -- > derivatives analyst
his name is Darth Vader
Here's a plausible example application for method pointers: you have a set of events that
require different processing for each event, but there are common parts of that processing
and data needs to be shared between the handlers.
For this, we could create a class that provides methods to handle each event along with methods
for shared processing and data members appropriate for the application. Now, an event
dispatcher provides a map with items:
{ eventId, [pointer to method for that id] }.
So event dispatching looks like this:
dispatcher[eventId](event args).
We will see an example in Chapter #5 - Classes.
4.8 Functors
Functors are instances of classes that implement operator() so they can be invoked.
In modern frameworks they often take the place of function pointers, e.g., used for
callbacks and to inject processing into some other class's instance.
class AFunctor {
public:
void operator()(const std::string& s);
// other members elided
}
private:
// member data elided
};
AFunctor fun;
fun("called like a function");
The Standard Template Library (STL) algorithms
were designed to be used with functors to inject processing into library defined operations,
like std::for_each. Here's an example:
struct display {
template<typename T>
void operator()(T t) {
std::cout << t << " ";
}
};
std::vector<std::string> coll{ "one", "two", "three", "four" };
std::for_each(coll.begin(), coll.end(), display);
std::for_each simply calls display on each of its elements. Since we
created the functor display as a template function, that will work for any collection
type for which elements can be inserted into std::cout.
This example code fragment is expanded to show all the parts in the details below.
functor syntax details
functors
class Functor {
public:
template<typename T>
void operator()(T element) {
++count_;
std::cout << "\n " << element;
}
size_t count() {
return count_;
}
void name(const std::string& nm) {
name_ = nm;
}
std::string name() {
return name_;
}
private:
std::string name_;
size_t count_ = 0;
};
Using functor:
Functor fun;
fun.name("counter");
std::vector<std::string> numbers{ "one", "two", "three", "four", "five" };
/* std::for_each invokes fun on each element in numbers */
/* it then returns a copy of fun to be interrogated later */
fun = std::for_each(numbers.begin(), numbers.end(), fun);
std::cout << "\n " << fun.name() << " processed "
<< fun.count() << " elements";
Output:
one
two
three
four
five
counter processed 5 elements
Functors are widely used in C++ code. In addition to being employed as presented here, they
are the basis for implementing lambda constructs, introduced with C++11.
4.9 Lambdas
Lambdas are locally defined callable objects that can capture state from their local
scope. That is often described as the lambda's closure. They are widely used
to inject processing into STL algorithms. You've already seen a couple of examples
of that in Chapter #1.
Lambda: anonymous locally defined functor with abreviated syntax
Lambda syntax example:
auto l1 = example() {
std::string s1 = "this is a demonstration";
auto lam = [s1](const std::string& s2) {
std::cout << "\n " << s1 << " of " << s2;
};
return lam;
};
l1("lambda syntax");
displays message "this is a demonstration of lambda sytax"
Here's what happened:
-
string s1 was constructed in example scope, e.g., the lambda's closure
-
lambda lam was defined capturing s1 by value
-
the lambda code sends a string to std::cout using the captured s1 copy
and a string, s2, passed by the using code as an invocation parameter
-
this created a lambda object, which will be executed later, and returned it to be copied to l1.
-
l1 executes the lambda passing it s2 = "lambda sytax"
Lambdas are used with STL algorithms, for providing threads with their processing semantics,
and used like stored scripts for handling message and event dispatching.
The details dropdown, below, defines capture and discusses syntax options for capture and return value.
Lambda Syntax:
Lambda Syntax - closure is local scope where lambda is defined
auto f = [capture specifier](argument list)[->optional return specification] {
body with code to execute
};
Capture specifier:
[] ==> no capture
[=] ==> capture all variables in closure by value
[&] ==> capture all variables in closure by reference
[v1, &v2] ==> capture, from closure, variable v1 by value and v2 by reference
argument list - same as function:
(T1 t1, T2 t2, ...) supplied by the caller
optional return specification:
Only needed if auto f can't deduce the return type.
body:
Same sytax as ordinary function, except that it may use captured variables as
well as variables from the parameter list, if any.
Note:
You need to be very careful with capture by reference and with capture of pointers by value.
If a lamda is passed out of its scope of definition, references and pointers will point to no longer
existing resources.
It is a good idea to use only specific capture specifiers for each captured variable used by the
the lambda code, like v1 and &v2, above. If you expect to return the lambda outside its scope
of definition, you would only use captured values, like v1, avoiding captures by reference like &v2.
I think you may be surprised how often you use lambdas once you get used to them.
Lambdas help to organize code by keeping the definition of a set of operations close
to the site where they are invoked.
4.10 Callable Objects
Any entity that can be invoked, e.g., functions, function pointers, methods, method pointers,
functors, and lambdas, are all referred to as callable objects. The STL algorithms accept
any of the STL containers and most accept any callable object to act on elements of the
container.
C++ threads accept any callable object that returns void.
The function std::invoke(...) accepts, as its first argument, any callable object.
If that is a method pointer, the next argument must be the address of an instance on which
the method pointer acts. Any remaining arguments - an arbitry finite number - are passed by
value to the callable object.
demo of std::invoke
std::invoke(f, "function via std::invoke", 1);
std::invoke(pFun, "function pointer via std::invoke", 2);
std::invoke(F(), "functor via std::invoke", 3);
std::invoke(lam, "lambda via std::invoke", 4)
std::invoke(pMethod, C(), "method pointer via std::invoke", 5);
In this example, f is a function taking a constant string reference and an unsigned int.
pFun is a function pointer to the same function.
F is a functor and F() is a temporary instance of F.
lam is a lambda.
C is a class with a method to call
and C() is an instance of C.
pMethod is a pointer to C's method.
A complete listing of this code is shown in the details below.
Complete Example
CallableObjects.cpp
#include <iostream>
#include <string>
#include <functional>
#include "../Display/Display.h"
std::string suffix(size_t i) {
std::string sfx;
switch (i)
{
case 1:
sfx = "st";
break;
case 2:
sfx = "nd";
break;
case 3:
sfx = "rd";
break;
default:
sfx = "th";
break;
}
return sfx;
}
void f(const std::string& type, size_t i) {
std::cout << "\n " << std::to_string(i)
<< suffix(i) + " invocation, a " + type;
}
void(*pFun)(const std::string&, size_t) = f;
class F {
public:
void operator()(const std::string& type, size_t i) {
std::cout << "\n " << std::to_string(i)
<< suffix(i) + " invocation, a " + type;
}
};
auto lam = [](const std::string& type, size_t i) {
std::cout << "\n " << std::to_string(i)
<< suffix(i) + " invocation, a " + type;
};
class C {
public:
void method(const std::string& type, size_t i) {
std::cout << "\n " << std::to_string(i)
<< suffix(i) + " invocation, a " + type;
}
};
using MPtr = void(C::*)(const std::string&, size_t);
MPtr pMethod = &C::method;
template<typename T>
void doInvoke(T t, const std::string& type, size_t count) {
t(type, count);
}
template<typename U, typename V>
void doInvoke(U u, V v, const std::string& type, size_t count) {
(u.*v)(type, count);
}
int main() {
displayDemo("--- Callable Objects Demo ---");
doInvoke(f, "function", 1);
doInvoke(pFun, "function pointer", 2);
doInvoke(F(), "functor", 3);
doInvoke(lam, "lambda", 4);
doInvoke(C(), pMethod, "method pointer", 4);
putline();
/*
std::invoke is more powerful than doInvoke as it takes an arbitry
number of arguments.
- the first may be a function, function pointer, or functor
that take any number of arguments
- the first may also be a method pointer. That requires the second
to be an instance of the class. It accepts an arbitrary number
of succeeding arguments.
That is implemented with a variadic template. Those will be discussed
in Chapter #4 - Templates.
*/
std::invoke(f, "function via std::invoke", 1);
std::invoke(pFun, "function pointer via std::invoke", 2);
std::invoke(F(), "functor via std::invoke", 3);
std::invoke(lam, "lambda via std::invoke", 4)
std::invoke(pMethod, C(), "method pointer via std::invoke", 5);
std::cout << "\n\n";
}
Output
--- Callable Objects Demo ---
1st invocation, a function
2nd invocation, a function pointer
3rd invocation, a functor
4th invocation, a lambda
5th invocation, a method pointer
1st invocation, a function via std::invoke
2nd invocation, a function pointer via std::invoke
3rd invocation, a functor via std::invoke
4th invocation, a lambda via std::invoke
5th invocation, a method pointer via std::invoke
Callable objects are used in many of the standard C++ libraries. Also, they are used frequently for
event handling and message dispatching. You will see examples of that in the next two chapters.
4.11 Passing Function and Method Parameters
Function and method arguments can be passed in one of two ways: by value or by reference. And, there are two ways
to pass by reference: using a C++ reference or by using a pointer.
When an argument is passed by value, it is copied onto the stackframe of the called function. For fundamental types
that is appropriate and commonly used. Since the function uses a copy of the parameter, any changes made in the
scope of the function will not affect the caller's value.
void fun(X x) { ... }
For large objects, however, the cost of making a copy is usually undesirable and pass by reference is used.
When an argument is passed by C++ reference, a reference is created in the function's stackframe, bound to the
parameter instance in the caller's scope.
void fun(X& x) { ... }
And, when an argument is passed by pointer reference, a copy of the pointer is copied into the function's
stackframe, bound to the paramter instance.
void fun(X* pX) { ... }
Pass by reference is normally implemented with C++ references, primarily because the syntax used in the
function body is simpler.
Note that the size of a reference is the same as the size of a pointer, so both will avoid the performance
penalty of copying a large object.
4.11.1 Side Effects
When an argument is passed by non-const reference to a function, the function may change the value in the
caller's scope. This is usually undesireable because it makes the caller's code harder to understand
and test. For that reason we usually pass by const reference:
void fun(const X& x) { ... }
or
void fun(const X* pX) { ... }
There are design cases where we elect to pass by non-const reference to use the resulting side effects, but
we should think carefully about this. There are, since C++14, simple ways to return multiple values from
a function, e.g., with std::tupls, so there are no longer many places we would elect to pass by
non-const reference.
4.12 Return Values
The type of a function or method return value has important consequences. If we are returning the value
of an instance declared within a function body we must not return that by reference. As soon as the
function call completes, all internally declared objects go out of scope, and are destroyed. If we return
a reference to one of them it will be invalid before the using code can do anything with it.
The value of a class data member from a method of that class may be returned by either value
or reference, since the data member continues to exist after the method call completes. The choice depends
on whether we intend the calling code to be able to modify the returned member datum. For non-const strings
the string indexer will return a reference to the indexed character. For const strings an overload of the
indexer will return the character by value.
4.12.1 Return Value Optimization
When a function or method returns by value an instance, defined internally, in order to initialize an instance of the
same type during construction, the compiler will often build the internal instance at the site of the receiving instance.
So, no copy is needed.
This is called Return Value Optimization (RVO). The example, below, demonstrates when RVO is
enabled.
Example: Return Value Optimization
Return value optimization
namespace Chap3 {
class X {
public:
X() {
std::cout << "\n default construction of ";
myCount_ = ++numObjs_;
std::cout << "object #" << myCount_;
}
X(const X& x) {
std::cout << "\n copy construction of ";
myCount_ = ++numObjs_;
std::cout << "object #" << myCount_;
}
X(X&& x) noexcept {
std::cout << "\n move construction of ";
myCount_ = ++numObjs_;
std::cout << "object #" << myCount_;
}
X& operator=(const X& x) {
std::cout << "\n copy assignment of ";
std::cout << "object #" << myCount_;
}
X& operator=(X&& x) noexcept {
std::cout << "\n move assignment of ";
std::cout << "object #" << myCount_;
}
~X() {
std::cout << "\n destruction of ";
std::cout << "object #" << myCount_;
}
size_t id() {
return myCount_;
}
private:
static size_t numObjs_;
size_t myCount_;
};
// static members must be defined outside class declaration
size_t X::numObjs_ = 0;
}
void showIn(const std::string& funName) {
std::cout << "\n entered " << funName;
}
void showOut(const std::string& funName) {
std::cout << "\n returned from " << funName;
}
Chap3::X test1() {
showIn("test1 - return with move");
Chap3::X x1;
return x1;
}
Chap3::X test2() {
showIn("test2 - return with RVO");
return Chap3::X();
}
Chap3::X test3() {
showIn("test3 - return with copy");
static Chap3::X x;
return x;
}
Using code:
int main() {
using namespace Chap3;
X x1 = test1();
showOut("test1");
putline();
X x2 = test2();
showOut("test2");
putline();
X x3 = test3();
showOut("test3");
std::cout << "\n\n";
}
Output:
entered test1
default construction of object #1
move construction of object #2
destruction of object #1
returned from test1
entered test2
default construction of object #3
returned from test2
entered test3
default construction of object #4
copy construction of object #5
returned from test3
destruction of object #5
destruction of object #3
destruction of object #2
destruction of object #4
So when does the compiler use RVO or move or copy to return value?
Move Construction
|
Returned instance is temporary constructed before return - test1 in example code.
|
Return Value Optimization (RVO)
|
Returned instance is temporary created in the return expression - test2 in example code.
|
Copy Construction
|
Returned instance is not a temporary, or no move constructor defined - test3 in example code.
|
Reference: Shaharmike.com
4.13 STL Algorithms
Most of the STL algorithms take a container range bounded by
[contr.begin(), contr.end()), where
contr is some STL container and begin() and end() return iterators to the
first element of the container and one past the last element.
Algorithms take a subsequent argument that defines what to do with each
element of the container.
In the example below, we use the std::copy_if algorithm which takes the input range and an
iterator, std::back_inserter, to insert elements into a destination container. Finally,
we pass a lambda expression to determine which elements to insert.
std::copy_if example
#include <algorithm>
#include <string>
#include <vector>
#include <iostream>
#include "../Display/Display.h"
template<typename T>
void show(T t) {
for (auto item : t)
std::cout << "\n " << item;
}
int main() {
std::vector<std::string> src{
"first string", "second str", "third collection", "another string"
};
std::vector<std::string> dst;
std::string s = "string"; // lambda capture
std::copy_if(
src.begin(), src.end(), // range of source to copy
std::back_inserter(dst), // insertion iterator push_backs into dst
[&s](const std::string& item) { // lambda defines src items to push_back
if (item.find(s) != std::string::npos) {
return false;
}
return true;
}
);
show(dst);
putline(2);
}
One could argue that it would be no more complex just to use a for loop to iterate over the container
instead of the copy_if algorithm. When to use the algorithms is largely a matter of taste.
However,
some of the algorithms implement quite complex operations and are the simplest choice when applicable. Also,
the algorithms are carefully designed to provide the fastest practical processing, and that is often a
distinct advantage.
4.14 Testing
All of the earlier sections in this chapter have explored facilities that C++ provides
to operate on data. This section looks at ways to verify that the operations deliver what we expect.
There are three kinds of testing needed for the code in this site's repositories:
-
Construction Tests:
These tests are implemented as an integral part of the implementation of a package's
code1. We add a few lines of code or a small function, then add a test to ensure that the
new code functions as expected. If we need more than one simple test to verify the new code, then
we aren't testing often enough. If the test fails, we know where to find the problem - in
the last few lines of code. The test code may be part of the package's main function or may
reside in a separate test package.
-
Unit Tests:
The intent of unit testing is to attempt to ensure that tested code meets all its obligations
in a robust manner. That entails testing every path through the code and testing boundary conditions,
e.g., beginning and end of the computational range, all cases that it may need to execute, and success or
failure when executing operations that may fail, like opening streams or connecting a socket.
Unit tests are labor intensive, and we may elect to unit test only those packages on which other
packages depend.
-
Regression Tests:
Regression tests are tests typically conducted over a library or large subsystem during their
implementation. Each regression test contains a set of test cases that are executed individually
usually in a predetermined sequence. It is very common to use a test harness to aggregate all the
tests and apply them to the library or subsystem whenever there is significant change. The idea is
to discover problems early that are due to changes in dependencies or the platform on which
the code executes.
-
Performance Tests:
Performance testing attempts to construct tests that:
-
Compare two processing streams satisfying the same obligations, to see which
has higher throughput, lower latency, or other performance metrics.
-
Attempt to make testing overhead a negligible part of the complete test process,
by pulling as much overhead as possible into initial and final activities that
are not included in measured outputs.
-
Run many times to amortize any remaining startup and shutdown, and average over
environmental effects that may have nothing to do with the comparison, but
happen to ocur during testing.
Often, a single iteration of a test may run fast enough that it is not possible to accurately
measure the time consumed, so running many iterations is also a way of improving measurement
accuracy.
For construction tests, we provide simple tests that are quick to write and don't require
a lot of analysis to build. For unit, regression, and performance tests we need to be more careful. These tests should
satisfy three properties:
-
Tests should be repeatable with the same results every time.
That implies that each test has a "setup" process that guarentees the testing
environment is in a fixed state at the beginning of testing. We may choose to do that with
an initialize function or may use a test class for each test that sets up the
environment in its constructor.
-
Test normal and abnormal conditions as completely as practical.
We do that by planning each test, defining input data to provide both expected and
possible but unexpected conditions. It helps to define functions:
-
Requires(pred)
defines condtions that are expected
to hold before an operation begins.
-
Ensures(pedicate)
defines condtions that are expected to hold after an operation.
-
Assert(predicate)
defines conditions that should be true at specific places in an operation.
where predicate is a boolean valued operation on the test environment and/or code
state.
-
Visualize operation results.
Evaluating all the conditions above often results in a lot of raw data about the
environment and code states. We need a way to selectively display that to a test
developer. That means we need a logging facility that can write to the console, to
test data files, or both. We want to be able to select the levels of display, so
we get very little output when the tests are running successfully, but with a lot
more detail when operations fail or are not as expected.
For these thorough tests it is common to write a test specification which clearly defines
the expected test results, initial setup, and any additional instructions for test developers
that may be needed (ideally none).
When unit or regression tests are concluded, a test report, generated by the logging
facility, is saved in the appropriate code repository. This should have a summary of
what passed and what failed, along with whatever data was logged during the final tests.
Below, find code declarations for a logger that provides the ability to record information
about the test in a "head" message, and then add additional log messages as needed.
Logger Code
Template Logger Code
enum Level { results = 1, demo = 2, debug = 4, all = 7 };
/*--- logger interface ------------------------------------------*/
template <typename T, size_t C = 0>
struct ILogger {
virtual ~ILogger() {}
virtual ILogger<T, C>& add(std::ostream*) = 0;
virtual ILogger<T, C>& write(T t, size_t level = Level::all) = 0;
virtual void head(T t = "") = 0;
virtual void prefix(T prfix = "\n ") = 0;
virtual void wait() = 0;
virtual void waitForWrites() = 0;
virtual void level(size_t lv) = 0;
virtual void name(const std::string& nm) = 0;
};
/*--- concrete logger -------------------------------------------*/
template <typename T, size_t C = 0>
class Logger : public ILogger<T, C> {
public:
Logger(const std::string& nm = "");
~Logger();
ILogger<T, C>& add(std::ostream* pOstrm);
virtual ILogger<T, C>& write(T t, size_t level = 0x7);
virtual void head(T t = "");
virtual void prefix(T prfix = "\n ");
virtual void level(size_t lv);
void name(const std::string& nm);
std::string name();
void wait();
void waitForWrites();
protected:
std::vector<std::ostream*> dstStrm;
BlockingQueue<T> blockingQueue_;
void threadProc();
std::string name_;
std::thread writeThread_;
T head_;
std::string prefix_ = "\n ";
size_t level_ = 0x7; // Level::debug + Level::demo + Level::results;
};
/*--- object factory ----------------------------------------------
*
* Creates static logger, so everyone calling makeLogger with
* the same value for C will use the same logger.
*/
template<typename T, size_t C>
inline ILogger<T, C>& makeLogger() {
static Logger<T, C> logger;
return logger;
}
Discussion of Logger Code
At the bottom of this code listing you will find an object factory that returns a reference
to a static logger, typed as an ILogger interface. That means that any part of the test code
that includes the logger header file, Logger.h, will be able to access a single static logger.
In case the test code needs two or more unique loggers, the logger's
template parameter C is used to define a category. Logger<T, 0> is a different type
than an instance of Logger<T, 1> so it does not share the same static logger.
The design of this logger includes a blocking queue, designed to receive log messages,
and a write thread that dequeues messages and writes them to the available streams, perhaps
the console and a test file. This was done so that logging minimizes write times for the
logging thread as much as practical.
Each call to the logger's write method accepts the message and a level. A test-wide
comparison level can be set with the method level(size_t lv). So if we set the test-wide
level to debug, any call to write(msg, lv) where lv sets a bit that matches one in the
test-wide level will be logged. Otherwise, it will not be recorded. The test-wide level
is set, by default, to match all of the cases: results, demo, and debug.
You can find all the code for this logger in the
CppStory C++ repository. Eventually it
will be moved to its own Logger repository.
Below find code for functions that will write to the console or throw an exception when an
unexpected condition occurs. Eventually these will use the logger instead of simply writing
to the console, but I need to use them in some significant testing to be sure how to configure
them before I make that change.
Requires, Ensures, and Assert
Requires, Ensures, and Assert
/*--- raised on unexpected condition ----------------------------*/
inline void Assert(
bool predicate,
const std::string& message = "",
size_t ln = 0,
bool doThrow = false
) {
if (predicate)
return;
std::string sentMsg = "Assertion raised";
if (ln > 0)
sentMsg += " at line number " + std::to_string(ln);
if (message.size() > 0)
sentMsg += "\n message: \"" + message + "\"";
if (doThrow)
throw std::exception(sentMsg.c_str());
else
std::cout << "\n " + sentMsg;
}
/*--- raised when input conditions are not satisfied ------------*/
inline void Requires(
bool predicate,
const std::string& message,
size_t lineNo, bool doThrow = false
) {
if (predicate)
return;
std::string sentMsg = "Requires " + message + " raised";
sentMsg += " at line number " + std::to_string(lineNo);
if (doThrow)
throw std::exception(sentMsg.c_str());
else
std::cout << "\n " + sentMsg;
}
/*--- raised when output conditions are not satisfied -----------*/
inline void Ensures(
bool predicate,
const std::string& message,
size_t lineNo,
bool doThrow = false
) {
if (predicate)
return;
std::string sentMsg = "Ensures " + message + " raised";
sentMsg += " at line number " + std::to_string(lineNo);
if (doThrow)
throw std::exception(sentMsg.c_str());
else
std::cout << "\n " + sentMsg;
}
4/15 Epilogue
We've looked at a number of fundamental programming techniques, using functions and other callable
objects. What we've learned here carries over into things we do when programming with C++, e.g.,
building classes and templates, and even compile-time programs using template metaprogramming.
This concludes Chapter #3 - Operations. In the next chapter we will look at details of
classes, their member data, and methods, using many ideas presented in this and the
previous chapter.
4/16 Programming Exercises
-
Write a function that accepts a std::string by const reference, efficiently reverses the string's
character sequence, and returns the reversed string by value.
Requires copy construction of temp std::string, looping half way through the string, and swapping characters
between the first half and the second half. What happens if the string has an odd number of characters?
-
Repeat the first exercise, but replace the std::string with a std::vector<char>.
Very similar to the first exercise.
-
Repeat the first exercise, but replace the std::string with a std::list<char>.
More complex than the first exercise.
-
Write a function that accepts std::vector<double> and displays its elements, where each element
is separated by a comma.
There should be no comma at the end.
-
Repeat the last exercise, but write each element in a fixed width field, where the field size
is a second parameter of the function. If the width is too small to hold the largest of the double elements, increase the size of the
field.
That will require you to step through the collection and convert each element to a std::string
representation (look up std::to_string), and find the largest.
4.12 References