about
10/19/2022
C++ Story Operations
C++ Story Code

Chapter #4 - C++ Operations

functions, methods, functors, lambdas, and callable objects

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.
  1. This complexity measure is closely related to the McCabe Cyclomatic Complexity metric. That has been discredited in an often quoted article in the academic literature. However, I believe that research was flawed - the metric values were correlated with change metrics and found to be weakly correlated. The authors concluded that Cyclomatic Complexity was therefore of little value.
    However, they didn't account for the fact that developers are very reluctant to change complex code because it is so hard to accomplish without introducing new errors. I contend that this result demonstrates the usefulness of the metric. In many years of developing code, I've found it to be very useful. Functions that have high complexity, by this measure, are very hard to understand and test.

4.5 Function Pointers

Function pointers are used to:
  1. create callbacks
  2. pass processing to platform API functions
  3. modify the way library functions operate, e.g., qsort accepts a comparator function pointer
  4. 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:
  1. string s1 was constructed in example scope, e.g., the lambda's closure
  2. lambda lam was defined capturing s1 by value
  3. 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
  4. this created a lambda object, which will be executed later, and returned it to be copied to l1.
  5. 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?
Return Action Conditions
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:
  1. 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.
  2. 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.
  3. 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.
  4. 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:
  1. 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.
  2. 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.
  3. 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; }

  1. A C++ package usually consists of a header file, [packageName].h, and implementation file [packageName].cpp. If the package is an executive with no dependency parents, then it consists of the single [executiveName].cpp file. Infrequently, we define utility packages with only a header file [utilityName].h.

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

  1. 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?
  2. Repeat the first exercise, but replace the std::string with a std::vector<char>.
    Very similar to the first exercise.
  3. Repeat the first exercise, but replace the std::string with a std::list<char>.
    More complex than the first exercise.
  4. 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.
  5. 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

  Next Prev Pages Sections About Keys