4.0 Operations Prologue
This chapter focuses on how C++ operations are implemented and tested - functions, methods,
lambdas, and more. The content applies to nearly everything you implement in C++.
Familiarity with these topics makes it easier to understand classes and templates in
succeeding chapters.
Quick Starter Example - Function Dispatching & Callbacks
This example shows that C++ functions can accept and return other functions.
The first line of code is a function pointer declaration - puzzling at first sight.
Function pointers are discussed later 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 returning function pointers is a powerful way to make designs flexible.
Conclusion:
Function callbacks and dispatch are tools most professional developers use fairly often.
This example dispatches functions and handles callbacks using function pointers. Substituting
lambdas for function pointers in arguments and return values moves the design closer to
functional programming. Lambdas can accept and return other lambdas, making them higher-order
callable objects.
Lambdas are const by default. Their state changes only when qualified as mutable.
Capturing values by value and accepting no arguments eliminates 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 for user-defined types use promotion constructors and cast operators.
Chapter 5 - Classes covers these in detail.
4.2 Copy Construction Operations
Copy construction creates a new instance of some type and initializes it with the state
of an existing instance of the same type. The examples below show copy construction
syntax for double 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 ] }
For structs and classes without a defined copy constructor, the compiler generates one
that performs member-wise copy operations. This works for arrays as well as fundamental
types. The compiler likely uses memcpy on contiguous array memory to perform that copy.
Chapter #5 - Classes covers compiler generated operations in detail.
4.3 Copy Assignment Operations
Copy assignment copies state from a source instance to an existing instance of the same
type. The examples below show copy assignment syntax for std::byte 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 ] }
For structs and classes without a defined copy assignment operator, the compiler generates
one that performs member-wise copy assignment. Chapter #5 - Classes covers compiler
generated operations in detail.
4.4 Functions
Functions have a name, zero or more arguments, and a return type or void, wrapped around
a block of code.
void putline(size_t n = 1) {
for(size_t i = 0; i < n; ++i)
std::cout << "\n";
}
Functions may have default arguments. Calling putline() without arguments pushes
a single newline to the terminal stream.
Arguments pass by value, as shown above, or by reference, as shown below.
void show(const std::vector<int>& vInt) {
for(int item : vInt)
std::cout << item << " ";
}
The & before vInt indicates the vector argument passes by reference.
Passing by const reference avoids side effects. Non-const references allow
a function to modify the caller's argument, but this makes code harder
to understand. Literals pass only by value or const reference.
Functions define only at namespace scope or class/struct scope. C++ does not support
inner functions. Functions defined in class or struct scope are methods of that class
or struct.
For the remainder of this document, method refers to any function defined in class or
struct scope. Functions at namespace scope are simply functions. When ambiguity arises,
the qualifiers global or unbound indicate non-method functions. putline is a global
function. A function name is a pointer to its code.
Each function should have a single responsibility and be small and simple enough to
understand and test. A good default size is 50 lines - that fits on a single page,
making all parts visible at once.
One useful complexity measure counts all scopes within a function body: 1 + the count
of all open braces "{" in the body1. This does not count the complexity of
called functions. A useful upper limit is CM = 10, with most functions targeting
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 applications can exist without function pointers, but existing frameworks -
especially platform APIs - often require them.
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
The syntax details below cover specific and generic function pointer declarations.
Several cases are shown because the syntax is complex.
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 methods. Methods have access to all member
data of the class. A class inheriting from base classes gains access to base class methods
and data qualified as public or protected, and to data qualified as protected.
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()
Class D inherits the two name methods from B, accessible to clients. It implements
the two occupation methods, also accessible to clients.
Derived class D contains its occupation_ string and the Base::name_ string, embedded
in a B image that is part of its memory footprint. The next chapter - Classes -
demonstrates this.
4.7 Method Pointers
Method pointers share the same uses as function pointers but have the advantage of
accessing member data from the invoking instance. They bind to any method of the
specified type, provided the arguments and return value match the method pointer
declaration.
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;
Binding to the inherited name functions requires the class specifier D::B, directing
the compiler to find the code in the base class definition. Binding to the occupation
names uses just the D class specifier. The syntax details below show all cases.
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
A plausible use case for method pointers: a set of events each require different
processing, but share common logic and data across handlers.
A class can provide methods to handle each event, shared processing methods, and
appropriate data members. An event dispatcher then provides a map with items:
{ eventId, [pointer to method for that id] }.
Event dispatching then looks like this:
dispatcher[eventId](event args).
Chapter #5 - Classes shows an example.
4.8 Functors
Functors are class instances that implement operator(), making them invocable.
In modern frameworks, they often replace function pointers for callbacks and for
injecting processing into other class instances.
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 accept functors to inject processing
into library-defined operations such as 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 calls display on each element. Creating display as a template
function makes it work for any collection type whose elements stream to
std::cout.
The details below expand this code fragment to show all parts.
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 appear widely in C++ code. Beyond their use here, they form the basis for
lambda constructs introduced in C++11.
4.9 Lambdas
Lambdas are locally defined callable objects that capture state from their enclosing
scope - their closure. They are widely used to inject processing into STL algorithms.
Chapter #1 showed a few examples.
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 work with STL algorithms, provide thread processing semantics, and serve as
stored scripts for 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.
Lambdas appear more often than expected once you become familiar with them. They
organize code by keeping operation definitions close to their invocation sites.
4.10 Callable Objects
Any entity that can be invoked - functions, function pointers, methods, method pointers,
functors, and lambdas - is a callable object. STL algorithms accept any STL container
and most accept any callable object to act on container elements.
C++ threads accept any callable object that returns void.
std::invoke(...) accepts any callable object as its first argument. When that is a
method pointer, the next argument must be an instance address on which the method
pointer acts. Any remaining arguments - an arbitrary number - pass 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 const 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.
lam is a lambda.
C is a class with a method, and C() is an instance.
pMethod points to C's method.
The details below show a complete listing.
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 appear throughout the standard C++ libraries and are used frequently
for event handling and message dispatching. The next two chapters show examples.
4.11 Passing Function and Method Parameters
Function and method arguments pass by value or by reference. Pass-by-reference uses
either a C++ reference or a pointer.
Passing by value copies the argument onto the called function's stack frame. For
fundamental types this is common and appropriate. Changes made within the function do
not affect the caller's value.
void fun(X x) { ... }
For large objects, copying is expensive. Pass by reference instead.
Passing by C++ reference creates a reference in the function's stack frame, bound
to the parameter in the caller's scope.
void fun(X& x) { ... }
Passing by pointer copies the pointer onto the function's stack frame, pointing
to the caller's parameter.
void fun(X* pX) { ... }
Pass by reference typically uses C++ references because the function body syntax is
simpler. A reference is the same size as a pointer, so both avoid the performance
cost of copying large objects.
4.11.1 Side Effects
Passing by non-const reference allows a function to change the caller's value.
This is usually undesirable because it makes the caller's code harder to understand
and test. Pass by const reference instead:
void fun(const X& x) { ... }
or
void fun(const X* pX) { ... }
Some designs pass by non-const reference to use the resulting side effects, but this
requires careful thought. Since C++14, functions can return multiple values using
std::tuple, so few cases remain where non-const references are preferable.
4.12 Return Values
Return value type has important consequences. Never return a locally declared instance
by reference. When a function call completes, all internal objects go out of scope and
are destroyed. A reference to one of them becomes invalid before the calling code can
use it.
A class data member may return by value or by reference because it persists after the
method call completes. The choice depends on whether calling code should modify the
returned datum. For non-const strings, the indexer returns a reference to the indexed
character. For const strings, an overload returns the character by value.
4.12.1 Return Value Optimization
When a function returns an internally defined instance by value to initialize an instance
of the same type, the compiler often constructs the internal instance at the receiving
site, eliminating the copy. This is Return Value Optimization (RVO).
The example below demonstrates when RVO applies.
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
When does the compiler use RVO, move, or copy to return a 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 STL algorithms take a container range bounded by
[contr.begin(), contr.end()), where
contr is an STL container and begin() and end() return iterators
to the first element and one past the last. A subsequent argument defines the
operation on each element.
The example below uses std::copy_if, which takes an input range and a
std::back_inserter iterator to push elements into a destination container. A lambda
expression determines which elements to include.
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);
}
Using a for loop instead of copy_if is no more complex. When to use the algorithms
is largely a matter of taste.
Some algorithms implement complex operations and are the simplest choice when
applicable. The algorithms are also designed for maximum practical performance,
which is a distinct advantage.
4.14 Testing
The earlier sections of this chapter explored facilities C++ provides for operating
on data. This section examines ways to verify that those operations deliver expected
results.
This site's code repositories need four kinds of testing:
-
Construction Tests:
Construction tests integrate into the package's implementation1. Add a few
lines or a small function, then add a test to confirm it works. If more than one simple
test is needed, the code units are too large. A failed test pinpoints the problem in
the last few lines of code. Test code can live in the package's main function or
a separate test package.
-
Unit Tests:
Unit testing verifies that code meets all its obligations robustly. This means
testing every path through the code and all boundary conditions - range endpoints,
all execution cases, and success or failure of operations that may fail, such as
opening streams or connecting a socket. Unit tests are labor intensive, so focus
them on packages that other packages depend on.
-
Regression Tests:
Regression tests run over a library or large subsystem during implementation.
Each regression test contains a set of test cases executed individually in a
predetermined sequence. A test harness aggregates all tests and applies them
whenever significant change occurs. The goal is to detect problems early when
dependencies or the platform changes.
-
Performance Tests:
Performance tests:
-
Compare two processing streams satisfying the same requirements to determine
which has higher throughput or lower latency.
-
Minimize test overhead by pulling it into initial and final activities outside
the measured window.
-
Run many iterations to amortize startup and shutdown costs and average out
environmental effects unrelated to the comparison.
A single iteration often runs too fast to measure accurately - multiple iterations
improve measurement accuracy.
Construction tests are quick to write and require little analysis. Unit, regression,
and performance tests need more care. These tests must satisfy three properties:
-
Tests should be repeatable with the same results every time.
Each test needs a setup process that places the environment in a fixed state before
testing. Use an initialize function or a test class whose constructor sets up the
environment.
-
Test normal and abnormal conditions as completely as practical.
Plan each test by defining input data for both expected and unexpected conditions.
Useful functions to define:
-
Requires(pred)
conditions expected to hold before an operation begins.
-
Ensures(predicate)
conditions expected to hold after an operation.
-
Assert(predicate)
conditions that must hold at specific points in an operation.
predicate is a boolean operation on the test environment or code state.
-
Visualize operation results.
Evaluating conditions produces significant raw data about the environment and
code state. A logging facility that selectively displays this to test developers
is essential - writing to the console, test data files, or both. Configurable
display levels produce minimal output when tests pass and detailed output when
operations fail.
Thorough tests typically require a test specification that defines expected results,
initial setup, and any additional instructions for test developers (ideally none).
When unit or regression tests conclude, the logging facility generates a test report
saved in the appropriate repository. The report summarizes what passed and failed,
with all data logged during the final tests.
Below are code declarations for a logger that records test information in a head
message and appends 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 is an object factory that returns a
reference to a static logger typed as an ILogger interface. Any test code
that includes Logger.h can access that single static logger.
When test code needs two or more unique loggers, the logger's
template parameter C defines a category. Logger<T, 0> is a different type
than Logger<T, 1>, so they do not share the same static logger.
The logger includes a blocking queue to receive log messages and a write
thread that dequeues and writes them to available streams - the console,
a test file, or both. This minimizes write time for the logging thread.
Each write call accepts the message and a level. The test-wide comparison
level sets via level(size_t lv). A call to write(msg, lv) logs only when
lv matches a bit in the test-wide level. The default matches all cases:
results, demo, and debug.
All logger code is in the
CppStory C++ repository. It will
move to its own Logger repository eventually.
Below are functions that write to the console or throw when an unexpected condition
occurs. Eventually they will use the logger instead of writing directly to the console,
but significant testing is needed to determine the right configuration before making
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
This chapter covered fundamental programming techniques using functions and other callable
objects. These techniques carry over into building classes and templates, and even
compile-time programs using template metaprogramming.
This concludes Chapter #4 - Operations. The next chapter covers classes - their member
data and methods - building on ideas from this and the preceding 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.
Copy-construct a temp std::string, loop halfway through, and swap characters
between the first and second halves. What happens with 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.
Step through the collection, convert each element to a std::string using
std::to_string, and find the largest.
4.17 References