9
FUNCTIONS

Functions should do one thing. They should do it well. They should do it only.
—Robert C. Martin,
Clean Code

Image

This chapter rounds out the ongoing discussion of functions, which encapsulate code into reusable components. Now that you’re armed with a strong background in C++ fundamentals, this chapter first revisits functions with a far more in-depth treatment of modifiers, specifiers, and return types, which appear in function declarations and specialize the behavior of your functions.

Then you’ll learn about overload resolution and accepting variable numbers of arguments before exploring function pointers, type aliases, function objects, and the venerable lambda expression. The chapter closes with an introduction to the std::function before revisiting the main function and accepting command line arguments.

Function Declarations

Function declarations have the following familiar form:

prefix-modifiers return-type func-name(arguments) suffix-modifiers;

You can provide a number of optional modifiers (or specifiers) to functions. Modifiers alter a function’s behavior in some way. Some modifiers appear at the beginning in the function’s declaration or definition (prefix modifiers), whereas others appear at the end (suffix modifiers). The prefix modifiers appear before the return type. The suffix modifiers appear after the argument list.

There isn’t a clear language reason why certain modifiers appear as prefixes or suffixes: because C++ has a long history, these features evolved incrementally.

Prefix Modifiers

At this point, you already know several prefix modifiers:

  • The prefix static indicates that a function that isn’t a member of a class has internal linkage, meaning the function won’t be used outside of this translation unit. Unfortunately, this keyword does double duty: if it modifies a method (that is, a function inside a class), it indicates that the function isn’t associated with an instantiation of the class but rather with the class itself (see Chapter 4).
  • The modifier virtual indicates that a method can be overridden by a child class. The override modifier indicates to the compiler that a child class intends to override a parent’s virtual function (see Chapter 5).
  • The modifier constexpr indicates that the function should be evaluated at compile time if possible (see Chapter 7).
  • The modifier [[noreturn]] indicates that this function won’t return (see Chapter 8). Recall that this attribute helps the compiler to optimize your code.

Another prefix modifier is inline, which plays a role in guiding the compiler when optimizing code.

On most platforms, a function call compiles into a series of instructions, such as the following:

  1. Place arguments into registers and on the call stack.
  2. Push a return address onto the call stack.
  3. Jump to the called function.
  4. After the function completes, jump to the return address.
  5. Clean up the call stack.

These steps typically execute very quickly, and the payoff in reduced binary size can be substantial if you use a function in many places.

Inlining a function means copying and pasting the contents of the function directly into the execution path, eliminating the need for the five steps outlined. This means that as the processor executes your code, it will immediately execute your function’s code rather than executing the (modest) ceremony required for function invocation. If you prefer this marginal increase in speed over the commensurate cost in increased binary size, you can use the inline keyword to indicate this to the compiler. The inline keyword hints to the compiler’s optimizer to put a function directly inline rather than perform a function call.

Adding inline to a function doesn’t change its behavior; it’s purely an expression of preference to the compiler. You must ensure that if you define a function inline, you do so in all translation units. Also note that modern compilers will typically inline functions where it makes sense—especially if a function isn’t used outside of a single translation unit.

Suffix Modifiers

At this point in the book, you already know two suffix modifiers:

  • The modifier noexcept indicates that the function will never throw an exception. It enables certain optimizations (see Chapter 4).
  • The modifier const indicates that the method won’t modify an instance of its class, allowing const references types to invoke the method (see Chapter 4).

This section explores three more suffix modifiers: final, override, and volatile.

final and override

The final modifier indicates that a method cannot be overridden by a child class. It’s effectively the opposite of virtual. Listing 9-1 attempts to override a final method and yields a compiler error.

#include <cstdio>

struct BostonCorbett {
  virtual void shoot() final {
    printf("What a God we have...God avenged Abraham Lincoln");
  }
};

struct BostonCorbettJunior : BostonCorbett {
  void shoot() override { } // Bang! shoot is final.
};

int main() {
  BostonCorbettJunior junior;
}

Listing 9-1: A class attempting to override a final method (This code doesn’t compile.)

This listing marks the shoot method final . Within BostonCorbettJunior, which inherits from BostonCorbett, you attempt to override the shoot method . This causes a compiler error.

You can also apply the final keyword to an entire class, disallowing that class from becoming a parent entirely, as demonstrated in Listing 9-2.

#include <cstdio>

struct BostonCorbett final  {
  void shoot()  {
    printf("What a God we have...God avenged Abraham Lincoln");
  }
};

struct BostonCorbettJunior : BostonCorbett  { }; // Bang!

int main() {
  BostonCorbettJunior junior;
}

Listing 9-2: A program with a class attempting to inherit from a final class. (This code doesn’t compile.)

The BostonCorbett class is marked as final , and this causes a compiler error when you attempt to inherit from it in BostonCorbettJunior .

NOTE

Neither final nor override is technically a language keyword; they are identifiers. Unlike keywords, identifiers gain special meaning only when used in a specific context. This means you can use final and override as symbol names elsewhere in your program, thereby leading to the insanity of constructions like virtual void final() override. Try not to do this.

Whenever you’re using interface inheritance, you should mark implementing classes final because the modifier can encourage the compiler to perform an optimization called devirtualization. When virtual calls are devirtualized, the compiler eliminates the runtime overhead associated with a virtual call.

volatile

Recall from Chapter 7 that a volatile object’s value can change at any time, so the compiler must treat all accesses to volatile objects as visible side effects for optimization purposes. The volatile keyword indicates that a method can be invoked on volatile objects. This is analogous to how const methods can be applied to const objects. Together, these two keywords define a method’s const/volatile qualification (or sometimes cv qualification), as demonstrated in Listing 9-3.

#include <cstdio>

struct Distillate {
  int apply() volatile  {
    return ++applications;
  }
private:
  int applications{};
};

int main() {
  volatile  Distillate ethanol;
  printf("%d Tequila
", ethanol.apply());
  printf("%d Tequila
", ethanol.apply());
  printf("%d Tequila
", ethanol.apply());
  printf("Floor!");
}
--------------------------------------------------------------------------
1 Tequila 
2 Tequila
3 Tequila
Floor!

Listing 9-3: Illustrating the use of a volatile method

In this listing, you declare the apply method on the Distillate class volatile . You also create a volatile Distillate called ethanol within main . Because the apply method is volatile, you can still invoke it (even though ethanol is volatile).

Had you not marked apply volatile , the compiler would emit an error when you attempted to invoke it . Just like you cannot invoke a non-const method on a const object, you cannot invoke a non-volatile method on a volatile object. Consider what would happen if you could perform such an operation: a non-volatile method is a candidate for all kinds of compiler optimizations for the reasons outlined in Chapter 7: many kinds of memory accesses can be optimized away without changing the observable side effects of your program.

How should the compiler treat a contradiction arising from you using a volatile object—which requires that all its memory accesses are treated as observable side effects—to invoke a non-volatile method? The compiler’s answer is that it calls this contradiction an error.

auto Return Types

There are two ways to declare the return value of a function:

  • (Primary) Lead a function declaration with its return type, as you’ve been doing all along.
  • (Secondary) Have the compiler deduce the correct return type by using auto.

As with auto type deduction, the compiler deduces the return type, fixing the runtime type.

This feature should be used judiciously. Because function definitions are documentation, it’s best to provide concrete return types when available.

auto and Function Templates

The primary use case for auto type deduction is with function templates, where a return type can depend (in potentially complicated ways) on the template parameters. Its usage is as follows:

auto my-function(arg1-type arg1, arg2-type arg2, ...) {
  // return any type and the
  // compiler will deduce what auto means
}

It’s possible to extend the auto-return-type deduction syntax to provide the return type as a suffix with the arrow operator ->. This way, you can append an expression that evaluates to the function’s return type. Its usage is as follows:

auto my-function(arg1-type arg1, arg2-type arg2, ...) -> type-expression {
  // return an object with type matching
  // the type-expression above
}

Usually, you wouldn’t use this pedantic form, but in certain situations it’s helpful. For example, this form of auto type deduction is commonly paired with a decltype type expression. A decltype type expression yields another expression’s resultant type. Its usage is as follows:

decltype(expression)

This expression resolves to the resulting type of the expression. For example, the following decltype expression yields int, because the integer literal 100 has that type:

decltype(100)

Outside of generic programming with templates, decltype is rare.

You can combine auto-return-type deduction and decltype to document the return types of function templates. Consider the add function in Listing 9-4, which defines a function template add that adds two arguments together.

#include <cstdio>

template <typename X, typename Y>
auto add(X x, Y y) -> decltype(x + y) { 
  return x + y;
}
int main() {
  auto my_double = add(100., -10);
  printf("decltype(double + int) = double; %f
", my_double); 

  auto my_uint = add(100U, -20);
  printf("decltype(uint + int) = uint; %u
", my_uint); 

  auto my_ulonglong = add(char{ 100 }, 54'999'900ull);
  printf("decltype(char + ulonglong) = ulonglong; %llu
", my_ulonglong); 
}
--------------------------------------------------------------------------
decltype(double + int) = double; 90.000000 
decltype(uint + int) = uint; 80 
decltype(char + ulonglong) = ulonglong; 55000000 

Listing 9-4: Using decltype and auto-return-type deduction

The add function employs auto type deduction with the decltype type expression . Each time you instantiate a template with two types X and Y, the compiler evaluates decltype(X + Y) and fixes the return type of add. Within main, you provide three instantiations. First, you add a double and an int . The compiler determines that decltype(double{ 100. } + int{ -10 }) is a double, which fixes the return type of this add instantiation. This, in turn, sets the type of my_double to double . You have two other instantiations: one for an unsigned int and int (which results in an unsigned int ) and another for a char and an unsigned long long (which results in an unsigned long long ).

Overload Resolution

Overload resolution is the process that the compiler executes when matching a function invocation with its proper implementation.

Recall from Chapter 4 that function overloads allow you to specify functions with the same name but different types and possibly different arguments. The compiler selects among these function overloads by comparing the argument types within the function invocation with the types within each overload declaration. The compiler will choose the best among the possible options, and if it cannot select a best option, it will generate a compiler error.

Roughly, the matching process proceeds as follows:

  1. The compiler will look for an exact type match.
  2. The compiler will try using integral and floating-point promotions to get a suitable overload (for example, int to long or float to double).
  3. The compiler will try to match using standard conversions like integral type to floating-point or casting a pointer-to-child into a pointer-to-parent.
  4. The compiler will look for a user-defined conversion.
  5. The compiler will look for a variadic function.

Variadic Functions

Variadic functions take a variable number of arguments. Typically, you specify the exact number of arguments a function takes by enumerating all of its parameters explicitly. With a variadic function, you can take any number of arguments. The variadic function printf is a canonical example: you provide a format specifier and an arbitrary number of parameters. Because printf is a variadic function, it accepts any number of parameters.

NOTE

The astute Pythonista will note an immediate conceptual relationship between variadic functions and *args/**kwargs.

You declare variadic functions by placing ... as the final parameter in the function’s argument list. When a variadic function is invoked, the compiler matches arguments against declared arguments. Any leftovers pack into the variadic arguments represented by the ... argument.

You cannot extract elements from the variadic arguments directly. Instead, you access individual arguments using the utility functions in the <cstdarg> header.

Table 9-1 lists these utility functions.

Table 9-1: Utility Functions in the <cstdarg> Header

Function

Description

va_list

Used to declare a local variable representing the variadic arguments

va_start

Enables access to the variadic arguments

va_end

Used to end iteration over the variadic arguments

va_arg

Used to iterate over each element in the variadic arguments

va_copy

Makes a copy of the variadic arguments

The utility functions’ usage is a little convoluted and best presented in a cohesive example. Consider the variadic sum function in Listing 9-5, which contains a variadic argument.

#include <cstdio>
#include <cstdint>
#include <cstdarg>

int sum(size_t n, ...) {
  va_list args; 
  va_start(args, n); 
  int result{};
  while (n--) {
    auto next_element = va_arg(args, int); 
      result += next_element;
  }
  va_end(args); 
  return result;
}

int main() {
  printf("The answer is %d.", sum(6, 2, 4, 6, 8, 10, 12)); 
}
--------------------------------------------------------------------------
The answer is 42. 

Listing 9-5: A sum function with a variadic argument list

You declare sum as a variadic function . All variadic functions must declare a va_list. You’ve named it args . A va_list requires initialization with va_start , which takes two arguments. The first argument is a va_list, and the second is the size of the variadic arguments. You iterate over each element in the variadic arguments using the va_args function. The first argument is the va_list argument, and the second is the argument type . Once you’ve completed iterating, you call va_list with the va_list structure .

You invoke sum with seven arguments: the first is the number of variadic arguments (six) followed by six numbers (2, 4, 6, 8, 10, 12) .

Variadic functions are a holdover from C. Generally, variadic functions are unsafe and a common source of security vulnerabilities.

There are at least two major problems with variadic functions:

  • Variadic arguments are not type-safe. (Notice that the second argument of va_args is a type.)
  • The number of elements in the variadic arguments must be tracked separately.

The compiler cannot help you with either of these issues.

Fortunately, variadic templates provide a safer and more performant way to implement variadic functions.

Variadic Templates

The variadic template enables you to create function templates that accept variadic, same-typed arguments. They enable you to employ the considerable power of the template engine. To declare a variadic template, you add a special template parameter called a template parameter pack. Listing 9-6 demonstrates its usage.

template <typename... Args>
return-type func-name(Args... args) {
  // Use parameter pack semantics
  // within function body
}

Listing 9-6: A template function with a parameter pack

The template parameter pack is part of the template parameter list . When you use Args within the function template , it’s called a function parameter pack. Some special operators are available for use with parameter packs:

  • You can use sizeof...(args) to obtain the parameter pack’s size.
  • You can invoke a function (for example, other_function) with the special syntax other_function(args...). This expands the parameter pack args and allows you to perform further processing on the arguments contained in the parameter pack.

Programming with Parameter Packs

Unfortunately, it’s not possible to index into a parameter pack directly. You must invoke the function template from within itself—a process called compile-time recursion—to recursively iterate over the elements in a parameter pack.

Listing 9-7 demonstrates the pattern.

template <typename T, typename...Args>
void my_func(T x, Args...args) {
  // Use x, then recurse:
  my_func(args...); 
}

Listing 9-7: A template function illustrating compile-time recursion with parameter packs. Unlike other usage listings, the ellipses contained in this listing are literal.

The key is to add a regular template parameter before the parameter pack . Each time you invoke my_func, x absorbs the first argument. The remainder packs into args. To invoke, you use the args... construct to expand the parameter pack .

The recursion needs a stopping criteria, so you add a function template specialization without the parameter:

template <typename T>
void my_func(T x) {
  // Use x, but DON'T recurse
}

Revisiting the sum Function

Consider the (much improved) sum function implemented as a variadic template in Listing 9-8.

#include <cstdio>

template <typename T>
constexpr T sum(T x) { 
    return x;
}

template <typename T, typename... Args>
constexpr T sum(T x, Args... args) { 
    return x + sum(args...);
}

int main() {
  printf("The answer is %d.", sum(2, 4, 6, 8, 10, 12)); 
}
--------------------------------------------------------------------------
The answer is 42. 

Listing 9-8: A refactor of Listing 9-5 using a template parameter pack instead of va_args

The first function is the overload that handles the stopping condition; if the function has only a single argument, you simply return the argument x, because the sum of a single element is just the element. The variadic template follows the recursion pattern outlined in Listing 9-7. It peels a single argument x off the parameter pack args and then returns x plus the result of the recursive call to sum with the expanded parameter pack . Because all of this generic programming can be computed at compile time, you mark these functions constexpr ➊➌. This compile-time computation is a major advantage over Listing 9-5, which has identical output but computes the result at runtime . (Why pay runtime costs when you don’t have to?)

When you just want to apply a single binary operator (like plus or minus) over a range of values (like Listing 9-5), you can use a fold expression instead of recursion.

Fold Expressions

A fold expression computes the result of using a binary operator over all the arguments of a parameter pack. Fold expressions are distinct from but related to variadic templates. Their usage is as follows:

(... binary-operator parameter-pack)

For example, you could employ the following fold expression to sum over all elements in a parameter pack called pack:

(... + args)

Listing 9-9 refactors 9-8 to use a fold expression instead of recursion.

#include <cstdio>

template <typename... T>
constexpr auto sum(T... args) {
  return (... + args); 
}
int main() {
  printf("The answer is %d.", sum(2, 4, 6, 8, 10, 12)); 
}
--------------------------------------------------------------------------
The answer is 42. 

Listing 9-9: A refactor of Listing 9-8 using a fold expression

You simplify the sum function by using a fold expression instead of the recursion approach . The end result is identical .

Function Pointers

Functional programming is a programming paradigm that emphasizes function evaluation and immutable data. One of the major concepts in functional programming is to pass a function as a parameter to another function.

One way you can achieve this is to pass a function pointer. Functions occupy memory, just like objects. You can refer to this memory address via usual pointer mechanisms. However, unlike objects, you cannot modify the pointed-to function. In this respect, functions are conceptually similar to const objects. You can take the address of functions and invoke them, and that’s about it.

Declaring a Function Pointer

To declare a function pointer, use the following ugly syntax:

return-type (*pointer-name)(arg-type1, arg-type2, ...);

This has the same appearance as a function declaration where the function name is replaced (*pointer-name).

As usual, you can employ the address-of operator & to take the address of a function. This is optional, however; you can simply use the function name as a pointer.

Listing 9-10 illustrates how you can obtain and use function pointers.

#include <cstdio>

float add(float a, int b) {
  return a + b;
}

float subtract(float a, int b) {
  return a - b;
}

int main() {
  const float first{ 100 };
  const int second{ 20 };

  float(*operation)(float, int) {}; 
  printf("operation initialized to 0x%p
", operation); 

  operation = &add; 
  printf("&add = 0x%p
", operation); 
  printf("%g + %d = %g
", first, second, operation(first, second)); 

  operation = subtract; 
  printf("&subtract = 0x%p
", operation); 
  printf("%g - %d = %g
", first, second, operation(first, second)); 
}
--------------------------------------------------------------------------
operation initialized to 0x0000000000000000 
&add = 0x00007FF6CDFE1070 
100 + 20 = 120 
&subtract = 0x00007FF6CDFE10A0 
100 - 20 = 80 

Listing 9-10: A program illustrating function pointers. (Due to address space layout randomization, the addresses ➍➐ will vary at runtime.)

This listing shows two functions with identical function signatures, add and subtract. Because the function signatures match, pointer types to these functions will also match. You initialize a function pointer operation accepting a float and an int as arguments and returning a float . Next, you print the value of operation, which is nullptr, after initialization .

You then assign the address of add to operation using the address-of operator and print its new address . You invoke operation and print the result .

To illustrate that you can reassign function pointers, you assign operation to subtract without using the address of operator , print the new value of operation , and finally print the result .

Type Aliases and Function Pointers

Type aliases provide a neat way to program with function pointers. The usage is as follows:

using alias-name = return-type(*)(arg-type1, arg-type2, ...)

You could have defined an operation_func type alias in Listing 9-10, for example:

using operation_func = float(*)(float, int);

This is especially useful if you’ll be using function pointers of the same type; it can really clean up the code.

The Function-Call Operator

You can make user-defined types callable or invocable by overloading the function-call operator operator()(). Such a type is called a function type, and instances of a function type are called function objects. The function-call operator permits any combination of argument types, return types, and modifiers (except static).

The primary reason you might want to make a user-defined type callable is to interoperate with code that expects function objects to use the function-call operator. You’ll find that many libraries, such as the stdlib, use the function-call operator as the interface for function-like objects. For example, in Chapter 19, you’ll learn how to create an asynchronous task with the std::async function, which accepts an arbitrary function object that can execute on a separate thread. It uses the function-call operator as the interface. The committee that invented std::async could have required you to expose, say, a run method, but they chose the function-call operator because it allows generic code to use identical notation to invoke a function or a function object.

Listing 9-11 illustrates the function-call operator’s usage.

struct type-name {
  return-type operator()(arg-type1 arg1, arg-type2 arg2, ...) {
    // Body of function-call operator
  }
}

Listing 9-11: The function-call operator’s usage

The function-call operator has the special operator() method name . You declare an arbitrary number of arguments , and you also decide the appropriate return type .

When the compiler evaluates a function-call expression, it will invoke the function-call operator on the first operand, passing the remaining operands as arguments. The result of the function-call expression is the result of invoking the corresponding function-call operator.

A Counting Example

Consider the function type CountIf in Listing 9-12, which computes the frequency of a particular char in a null-terminated string.

#include <cstdio>
#include <cstdint>

struct CountIf {
  CountIf(char x) : x{ x } { }
  size_t operator()(const char* str) const {
    size_t index{}, result{};
    while (str[index]) {
      if (str[index] == x) result++; 
      index++;
    }
    return result;
  }
private:
  const char x;
};

int main() {
  CountIf s_counter{ 's' }; 
  auto sally = s_counter("Sally sells seashells by the seashore."); 
  printf("Sally: %zd
", sally);
  auto sailor = s_counter("Sailor went to sea to see what he could see.");
  printf("Sailor: %zd
", sailor);
  auto buffalo = CountIf{ 'f' }("Buffalo buffalo Buffalo buffalo "
                                "buffalo buffalo Buffalo buffalo."); 
  printf("Buffalo: %zd
", buffalo);
}
--------------------------------------------------------------------------
Sally: 7
Sailor: 3
Buffalo: 16

Listing 9-12: A function type that counts the number of characters appearing in a null-terminated string

You initialize CountIf objects using a constructor taking a char . You can call the resulting function object as if it were a function taking a null-terminated string argument , because you’ve implemented the function call operator. The function call operator iterates through each character in the argument str using an index variable , incrementing the result variable whenever the character matches the x field . Because calling the function doesn’t modify the state of a CountIf object, you’ve marked it const.

Within main, you’ve initialized the CountIf function object s_counter, which will count the frequency of the letter s . You can use s_counter as if it were a function . You can even initialize a CountIf object and use the function operator directly as an rvalue object . You might find this convenient to do in some settings where, for example, you might only need to invoke the object a single time.

You can employ function objects as partial applications. Listing 9-12 is conceptually similar to the count_if function in Listing 9-13.

#include <cstdio>
#include <cstdint>

size_t count_if(char x, const char* str) {
  size_t index{}, result{};
  while (str[index]) {
    if (str[index] == x) result++;
    index++;
  }
  return result;
}

int main() {
  auto sally = count_if('s', "Sally sells seashells by the seashore.");
  printf("Sally: %zd
", sally);
  auto sailor = count_if('s', "Sailor went to sea to see what he could see.");
  printf("Sailor: %zd
", sailor);
  auto buffalo = count_if('f', "Buffalo buffalo Buffalo buffalo "
                               "buffalo buffalo Buffalo buffalo.");
  printf("Buffalo: %zd
", buffalo);
}
--------------------------------------------------------------------------
Sally: 7
Sailor: 3
Buffalo: 16

Listing 9-13: A free function emulating Listing 9-12

The count_if function has an extra argument x , but otherwise it’s almost identical to the function operator of CountIf.

NOTE

In functional programming parlance, the CountIf is the partial application of x to count_if. When you partially apply an argument to a function, you fix that argument’s value. The product of such a partial application is another function taking one less argument.

Declaring function types is verbose. You can often reduce the boilerplate substantially with lambda expressions.

Lambda Expressions

Lambda expressions construct unnamed function objects succinctly. The function object implies the function type, resulting in a quick way to declare a function object on the fly. Lambdas don’t provide any additional functionality other than declaring function types the old-fashioned way. But they’re extremely convenient when you need to initialize a function object in only a single context.

Usage

There are five components to a lambda expression:

  • captures: The member variables of the function object (that is, the partially applied parameters)
  • parameters: The arguments required to invoke the function object
  • body: The function object’s code
  • specifiers: Elements like constexpr, mutable, noexcept, and [[noreturn]]
  • return type: The type returned by the function object

Lambda expression usage is as follows:

[captures] (parameters) modifiers -> return-type { body }

Only the captures and the body are required; everything else is optional. You’ll learn about each of these components in depth in the next few sections.

Each lambda component has a direct analogue in a function object. To form a bridge between the function objects like CountIf and lambda expressions, look at Listing 9-14, which lists the CountIf function type from Listing 9-12 with annotations that correspond to the analogous portions of the lambda expression in the usage listing.

struct CountIf {
  CountIf(char x) : x{ x } { } 
  size_t operator()(const char* str) const {
    --snip--
  }
private:
  const char x; 
};

Listing 9-14: Comparing the CountIf type declaration with a lambda expression

The member variables you set in the constructor of CountIf are analogous to a lambda’s capture . The function-call operator’s arguments , body , and return type are analogous to the lambda’s parameters, body, and return type. Finally, modifiers can apply to the function-call operator and the lambda. (The numbers in the Lambda expession usage example and Listing 9-14 correspond.)

Lambda Parameters and Bodies

Lambda expressions produce function objects. As function objects, lambdas are callable. Most of the time, you’ll want your function object to accept parameters upon invocation.

The lambda’s body is just like a function body: all of the parameters have function scope.

You declare lambda parameters and bodies using essentially the same syntax that you use for functions.

For example, the following lambda expression yields a function object that will square its int argument:

[](int x) { return x*x; }

The lambda takes a single int x and uses it within the lambda’s body to perform the squaring.

Listing 9-15 employs three different lambdas to transform the array 1, 2, 3.

#include <cstdio>
#include <cstdint>

template <typename Fn>
void transform(Fn fn, const int* in, int* out, size_t length) { 
  for(size_t i{}; i<length; i++) {
    out[i] = fn(in[i]); 
  }
}

int main() {
  const size_t len{ 3 };
  int base[]{ 1, 2, 3 }, a[len], b[len], c[len];
  transform([](int x) { return 1; }, base, a, len);
  transform([](int x) { return x; }, base, b, len);
  transform([](int x) { return 10*x+5; }, base, c, len);
  for (size_t i{}; i < len; i++) {
    printf("Element %zd: %d %d %d
", i, a[i], b[i], c[i]);
  }
}
--------------------------------------------------------------------------
Element 0: 1 1 15
Element 1: 1 2 25
Element 2: 1 3 35

Listing 9-15: Three lambdas and a transform function

The transform template function accepts four arguments: a function object fn, an in array and an out array, and the corresponding length of those arrays. Within transform, you invoke fn on each element of in and assign the result to the corresponding element of out .

Within main, you declare a base array 1, 2, 3 that will be used as the in array. In the same line you also declare three uninitialized arrays a, b, and c, which will be used as the out arrays. The first call to transform passes a lambda ([](int x) { return 1; }) that always returns 1 , and the result is stored into a. (Notice that the lambda didn’t need a name!) The second call to transform ([](int x) { return x; }) simply returns its argument , and the result is stored into b. The third call to transform multiplies the argument by 10 and adds 5 . The result is stored in c. You then print the output into a matrix where each column illustrates the transform that was applied to the different lambdas in each case.

Notice that you declared transform as a template function, allowing you to reuse it with any function object.

Default Arguments

You can provide default arguments to a lambda. Default lambda parameters behave just like default function parameters. The caller can specify values for default parameters, in which case the lambda uses the caller-provided values. If the caller doesn’t specify a value, the lambda uses the default.

Listing 9-16 illustrates the default argument behavior.

#include <cstdio>

int main() {
  auto increment = [](auto x, int y = 1) { return x + y; };
  printf("increment(10)    = %d
", increment(10)); 
  printf("increment(10, 5) = %d
", increment(10, 5)); 
}
--------------------------------------------------------------------------
increment(10)    = 11 
increment(10, 5) = 15 

Listing 9-16: Using default lambda parameters

The increment lambda has two parameters, x and y. But the y parameter is optional because it has the default argument 1 . If you don’t specify an argument for y when you call the function ➋, increment returns 1 + x. If you do call the function with an argument for y , that value is used instead.

Generic Lambdas

Generic lambdas are lambda expression templates. For one or more parameters, you specify auto rather than a concrete type. These auto types become template parameters, meaning the compiler will stamp out a custom instantiation of the lambda.

Listing 9-17 illustrates how to assign a generic lambda into a variable and then use the lambda in two different template instantiations.

#include <cstdio>
#include <cstdint>

template <typename Fn, typename T>
void transform(Fn fn, const T* in, T* out, size_t len) {
  for(size_t i{}; i<len; i++) {
    out[i] = fn(in[i]);
  }
}

int main() {
  constexpr size_t len{ 3 };
  int base_int[]{ 1, 2, 3 }, a[len]; 
  float base_float[]{ 10.f, 20.f, 30.f }, b[len]; 
  auto translate = [](auto x) { return 10 * x + 5; }; 
  transform(translate, base_int, a, l); 
  transform(translate, base_float, b, l); 

  for (size_t i{}; i < l; i++) {
    printf("Element %zd: %d %f
", i, a[i], b[i]);
  }
}
--------------------------------------------------------------------------
Element 0: 15 105.000000
Element 1: 25 205.000000
Element 2: 35 305.000000

Listing 9-17: Using a generic lambda

You add a second template parameter to transform , which you use as the pointed-to type of in and out. This allows you to apply transform to arrays of any type, not just of int types. To test out the upgraded transform template, you declare two arrays with different pointed-to types: int and float . (Recall from Chapter 3 that the f in 10.f specifies a float literal.) Next, you assign a generic lambda expression to translate . This allows you to use the same lambda for each instantiation of transform: when you instantiate with base_int and with base_float .

Without a generic lambda, you’d have to declare the parameter types explicitly, like the following:

--snip–
  transform([](int x) { return 10 * x + 5; }, base_int, a, l); 
  transform([](double x) { return 10 * x + 5; }, base_float, b, l); 

So far, you’ve been leaning on the compiler to deduce the return types of your lambdas. This is especially useful for generic lambdas, because often the lambda’s return type will depend on its parameter types. But you can explicitly state the return type if you want.

Lambda Return Types

The compiler deduces a lambda’s return type for you. To take over from the compiler, you use the arrow -> syntax, as in the following:

[](int x, double y) -> double { return x + y; }

This lambda expression accepts an int and a double and returns a double.

You can also use decltype expressions, which can be useful with generic lambdas. For example, consider the following lambda:

[](auto x, double y) -> decltype(x+y) { return x + y; }

Here you’ve explicitly declared that the return type of the lambda is whatever type results from adding an x to a y.

You’ll rarely need to specify a lambda’s return type explicitly.

A far more common requirement is that you must inject an object into a lambda before invocation. This is the role of lambda captures.

Lambda Captures

Lambda captures inject objects into the lambda. The injected objects help to modify the behavior of the lambda.

Declare a lambda’s capture by specifying a capture list within brackets []. The capture list goes before the parameter list, and it can contain any number of comma-separated arguments. You then use these arguments within the lambda’s body.

A lambda can capture by reference or by value. By default, lambdas capture by value.

A lambda’s capture list is analogous to a function type’s constructor. Listing 9-18 reformulates CountIf from Listing 9-12 as the lambda s_counter.

#include <cstdio>
#include <cstdint>

int main() {
  char to_count{ 's' }; 
  auto s_counter = [to_count](const char* str) {
    size_t index{}, result{};
    while (str[index]) {
      if (str[index] == to_count) result++;
      index++;
    }
    return result;
  };
  auto sally = s_counter("Sally sells seashells by the seashore.");
  printf("Sally: %zd
", sally);
  auto sailor = s_counter("Sailor went to sea to see what he could see.");
  printf("Sailor: %zd
", sailor);
}
--------------------------------------------------------------------------
Sally: 7
Sailor: 3

Listing 9-18: Reformulating CountIf from Listing 9-12 as a lambda

You initialize a char called to_count to the letter s . Next, you capture to_count within the lambda expression assigned to s_counter . This makes to_count available within the body of the lambda expression .

To capture an element by reference rather than by value, prefix the captured object’s name with an ampersand &. Listing 9-19 adds a capture reference to s_counter that keeps a running tally across lambda invocations.

#include <cstdio>
#include <cstdint>

int main() {
  char to_count{ 's' };
  size_t tally{};
  auto s_counter = [to_count, &tally](const char* str) {
    size_t index{}, result{};
    while (str[index]) {
      if (str[index] == to_count) result++;
      index++;
    }
    tally += result;
    return result;
  };
  printf("Tally: %zd
", tally); 
  auto sally = s_counter("Sally sells seashells by the seashore.");
  printf("Sally: %zd
", sally);
  printf("Tally: %zd
", tally); 
  auto sailor = s_counter("Sailor went to sea to see what he could see.");
  printf("Sailor: %zd
", sailor);
  printf("Tally: %zd
", tally); 
}
--------------------------------------------------------------------------
Tally: 0 
Sally: 7
Tally: 7 
Sailor: 3
Tally: 10 

Listing 9-19: Using a capture reference in a lambda

You initialize the counter variable tally to zero , and then the s_counter lambda captures tally by reference (note the ampersand &) . Within the lambda’s body, you add a statement to increment tally by an invocation’s result before returning . The result is that tally will track the total count no matter how many times you invoke the lambda. Before the first s_counter invocation, you print the value of tally (which is still zero). After you invoke s_counter with Sally sells seashells by the seashore., you have a tally of 7 . The last invocation of s_counter with Sailor went to sea to see what he could see. returns 3, so the value of tally is 7 + 3 = 10 .

Default Capture

So far, you’ve had to capture each element by name. Sometimes this style of capturing is called named capture. If you’re lazy, you can capture all automatic variables used within a lambda using default capture. To specify a default capture by value within a capture list, use a lone equal sign =. To specify a default capture by reference, use a lone ampersand &.

For example, you could “simplify” the lambda expression in Listing 9-19 to perform a default capture by reference, as demonstrated in Listing 9-20.

--snip--
  auto s_counter = [&](const char* str) {
    size_t index{}, result{};
    while (str[index]) {
      if (str[index] == to_count) result++;
      index++;
    }
    tally += result;
    return result;
  };
--snip--

Listing 9-20: Simplifying a lambda expression with a default capture by reference

You specify a default capture by reference , which means any automatic variables in the body of the lambda expression get captured by reference. There are two: to_count and tally .

If you compile and run the refactored listing, you’ll obtain identical output. However, notice that to_count is now captured by reference. If you accidentally modify it within the lambda expression’s body, the change will occur across lambda invocations as well as within main (where to_count is an automatic variable).

What would happen if you performed a default capture by value instead? You would only need to change the = to an & in the capture list, as demonstrated in Listing 9-21.

--snip--
  auto s_counter = [=](const char* str) {
    size_t index{}, result{};
    while (str[index]) {
      if (str[index] == to_count) result++;
      index++;
    }
    tally += result;
    return result;
  };
--snip--

Listing 9-21: Modifying Listing 9-20 to capture by value instead of by reference (This code doesn't compile.)

You change the default capture to be by value . The to_count capture is unaffected , but attempting to modify tally results in a compiler error . You’re not allowed to modify variables captured by value unless you add the mutable keyword to the lambda expression. The mutable keyword allows you to modify value-captured variables. This includes calling non-const methods on that object.

Listing 9-22 adds the mutable modifier and has a default capture by value.

#include <cstdio>
#include <cstdint>

int main() {
  char to_count{ 's' };
  size_t tally{};
  auto s_counter = [=](const char* str) mutable {
    size_t index{}, result{};
    while (str[index]) {
      if (str[index] == to_count) result++;
      index++;
    }
    tally += result;
    return result;
  };
  auto sally = s_counter("Sally sells seashells by the seashore.");
  printf("Tally: %zd
", tally); 
  printf("Sally: %zd
", sally);
  printf("Tally: %zd
", tally); 
  auto sailor = s_counter("Sailor went to sea to see what he could see.");
  printf("Sailor: %zd
", sailor);
  printf("Tally: %zd
", tally); 
}
--------------------------------------------------------------------------
Tally: 0
Sally: 7
Tally: 0
Sailor: 3
Tally: 0

Listing 9-22: A mutable lambda expression with a default capture by value

You declare a default capture by value , and you make the lambda s_counter mutable . Each of the three times you print tally ➌➍➎, you get a zero value. Why?

Because tally gets copied by value (via the default capture), the version in the lambda is, in essence, an entirely different variable that just happens to have the same name. Modifications to the lambda’s copy of tally don’t affect the automatic tally variable of main. The tally in main() is initialized to zero and never gets modified.

It’s also possible to mix a default capture with a named capture. You could, for example, default capture by reference and copy to_count by value using the following formulation:

  auto s_counter = [&,to_count](const char* str) {
    --snip--
  };

This specifies a default capture by reference and to_count capture by value.

Although performing a default capture might seem like an easy shortcut, refrain from using it. It’s far better to declare captures explicitly. If you catch yourself saying “I’ll just use a default capture because there are too many variables to list out,” you probably need to refactor your code.

Initializer Expressions in Capture Lists

Sometimes you want to initialize a whole new variable within a capture list. Maybe renaming a captured variable would make a lambda expression’s intent clearer. Or perhaps you want to move an object into a lambda and therefore need to initialize a variable.

To use an initializer expression, just declare the new variable’s name followed by an equal sign and the value you want to initialize your variable with, as Listing 9-23 demonstrates.

  auto s_counter = [&tally,my_char=to_count](const char* str) {
    size_t index{}, result{};
    while (str[index]) {
      if (str[index] == my_char) result++;
    --snip--
  };

Listing 9-23: Using an initializer expression within a lambda capture

The capture list contains a simple named capture where you have tally by reference . The lambda also captures to_count by value, but you’ve elected to use the variable name my_char instead . Of course, you’ll need to use the name my_char instead of to_count inside the lambda .

NOTE

An initializer expression in a capture list is also called an init capture.

Capturing this

Sometimes lambda expressions have an enclosing class. You can capture an enclosing object (pointed-to by this) by value or by reference using either [*this] or [this], respectively.

Listing 9-24 implements a LambdaFactory that generates counting lambdas and keeps track of a tally.

#include <cstdio>
#include <cstdint>

struct LambdaFactory {
  LambdaFactory(char in) : to_count{ in }, tally{} { }
  auto make_lambda() { 
    return [this](const char* str) {
      size_t index{}, result{};
      while (str[index]) {
        if (str[index] == to_count) result++;
        index++;
      }
      tally += result;
      return result;
    };
  }
  const char to_count;
  size_t tally;
};

int main() {
  LambdaFactory factory{ 's' }; 
  auto lambda = factory.make_lambda(); 
  printf("Tally: %zd
", factory.tally);
  printf("Sally: %zd
", lambda("Sally sells seashells by the seashore."));
  printf("Tally: %zd
", factory.tally);
  printf("Sailor: %zd
", lambda("Sailor went to sea to see what he could see."));
  printf("Tally: %zd
", factory.tally);
}
--------------------------------------------------------------------------
Tally: 0
Sally: 7
Tally: 7
Sailor: 3
Tally: 10

Listing 9-24: A LambdaFactory illustrating the use of this capture

The LambdaFactory constructor takes a single character and initializes the to_count field with it. The make_lambda method illustrates how you can capture this by reference and use the to_count and tally member variables within the lambda expression.

Within main, you initialize a factory and make a lambda using the make_lambda method . The output is identical to Listing 9-19, because you capture this by reference and state of tally persists across invocations of lambda.

Clarifying Examples

There are a lot of possibilities with capture lists, but once you have a command of the basics–capturing by value and by reference–there aren’t many surprises. Table 9-2 provides short, clarifying examples that you can use for future reference.

Table 9-2: Clarifying Examples of Lambda Capture Lists

Capture list

Meaning

[&]

Default capture by reference

[&,i]

Default capture by reference; capture i by value

[=]

Default capture by value

[=,&i]

Default capture by value; capture i by reference

[i]

Capture i by value

[&i]

Capture i by reference

[i,&j]

Capture i by value; capture j by reference

[i=j,&k]

Capture j by value as i; capture k by reference

[this]

Capture enclosing object by reference

[*this]

Capture enclosing object by value

[=,*this,i,&j]

Default capture by value; capture this and i by value; capture j by reference

constexpr Lambda Expressions

All lambda expressions are constexpr as long as the lambda can be invoked at compile time. You can optionally make the constexpr declaration explicit, as in the following:

[] (int x) constexpr { return x * x; }

You should mark a lambda constexpr if you want to make sure that it meets all constexpr requirements. As of C++17, this means no dynamic memory allocations and no calling non-constexpr functions, among other restrictions. The standards committee plans to loosen these restrictions with each release, so if you write a lot of code using constexpr, be sure to brush up on the latest constexpr constraints.

std::function

Sometimes you just want a uniform container for storing callable objects. The std::function class template from the <functional> header is a polymorphic wrapper around a callable object. In other words, it’s a generic function pointer. You can store a static function, a function object, or a lambda into a std::function.

NOTE

The function class is in the stdlib. We’re presenting it a little ahead of schedule because it fits naturally.

With functions, you can:

  • Invoke without the caller knowing the function’s implementation
  • Assign, move, and copy
  • Have an empty state, similar to a nullptr

Declaring a Function

To declare a function, you must provide a single template parameter containing the function prototype of the callable object:

std::function<return-type(arg-type-1, arg-type-2, etc.)>

The std::function class template has a number of constructors. The default constructor constructs a std::function in empty mode, meaning it contains no callable object.

Empty Functions

If you invoke a std::function with no contained object, std::function will throw a std::bad_function_call exception. Consider Listing 9-25.

#include <cstdio>
#include <functional>

int main() {
    std::function<void()> func; 
    try {
        func(); 
    } catch(const std::bad_function_call& e) {
        printf("Exception: %s", e.what()); 
    }
}
--------------------------------------------------------------------------
Exception: bad function call 

Listing 9-25: The default std::function constructor and the std::bad_function_call exception

You default-construct a std::function . The template parameter void() denotes a function taking no arguments and returning void. Because you didn’t fill func with a callable object, it’s in an empty state. When you invoke func , it throws a std::bad_function_call, which you catch and print .

Assigning a Callable Object to a Function

To assign a callable object to a function, you can either use the constructor or assignment operator of function, as in Listing 9-26.

#include <cstdio>
#include <functional>

void static_func() { 
  printf("A static function.
");
}

int main() {
  std::function<void()> func { [] { printf("A lambda.
"); } }; 
  func(); 
  func = static_func; 
  func(); 
}
--------------------------------------------------------------------------
A lambda. 
A static function. 

Listing 9-26: Using the constructor and assignment operator of function

You declare the static function static_func that takes no arguments and returns void . In main, you create a function called func . The template parameter indicates that a callable object contained by func takes no arguments and returns void. You initialize func with a lambda that prints the message A lambda. You invoke func immediately afterward , invoking the contained lambda and printing the expected message. Next, you assign static_func to func, which replaces the lambda you assigned upon construction . You then invoke func, which invokes static_func rather than the lambda, so you see A static function. printed .

An Extended Example

You can construct a function with callable objects, as long as that object supports the function semantics implied by the template parameter of function.

Listing 9-27 uses an array of std::function instances and fills it with a static function that counts spaces, a CountIf function object from Listing 9-12, and a lambda that computes string length.

#include <cstdio>
#include <cstdint>
#include <functional>

struct CountIf {
  --snip--
};

size_t count_spaces(const char* str) {
  size_t index{}, result{};
  while (str[index]) {
    if (str[index] == ' ') result++;
    index++;
  }
  return result;
}

std::function<size_t(const char*)> funcs[]{
  count_spaces, 
  CountIf{ 'e' }, 
  [](const char* str) { 
    size_t index{};
    while (str[index]) index++;
    return index;
  }
};

auto text = "Sailor went to sea to see what he could see.";

int main() {
  size_t index{};
  for(const auto& func : funcs) {
    printf("func #%zd: %zd
", index++, func(text));
  }
}
--------------------------------------------------------------------------
func #0: 9 
func #1: 7 
func #2: 44 

Listing 9-27: Using a std::function array to iterate over a uniform collection of callable objects with varying underlying types

You declare a std::function array with static storage duration called funcs. The template argument is the function prototype for a function taking a const char* and returning a size_t . In the funcs array, you pass in a static function pointer , a function object , and a lambda . In main, you use a range-based for loop to iterate through each function in funcs . You invoke each function func with the text Sailor went to sea to see what he could see. and print the result.

Notice that, from the perspective of main, all the elements in funcs are the same: you just invoke them with a null-terminated string and get back a size_t .

NOTE

Using a function can incur runtime overhead. For technical reasons, function might need to make a dynamic allocation to store the callable object. The compiler also has difficulty optimizing away function invocations, so you’ll often incur an indirect function call. Indirect function calls require additional pointer dereferences.

The main Function and the Command Line

All C++ programs must contain a global function with the name main. This function is defined as the program’s entry point, the function invoked at program startup. Programs can accept any number of environment-provided arguments called command line parameters upon startup.

Users pass command line parameters to programs to customize their behavior. You’ve probably used this feature when executing command line programs, as in the copy (on Linux: cp) command:

$ copy file_a.txt file_b.txt

When invoking this command, you instruct the program to copy file_a.txt into file_b.txt by passing these values as command line parameters. As with command line programs you might be used to, it’s possible to pass values as command line parameters to your C++ programs.

You can choose whether your program handles command line parameters by how you declare main.

The Three main Overloads

You can access command line parameters within main by adding arguments to your main declaration.

There are three valid varieties of overload for main, as shown in Listing 9-28.

int main(); 
int main(int argc, char* argv[]); 
int main(int argc, char* argv[], impl-parameters); 

Listing 9-28: The valid overloads for main

The first overload takes no parameters, which is the way you’ve been using main() in this book so far. Use this form if you want to ignore any arguments provided to your program.

The second overload accepts two parameters, argc and argv. The first argument, argc, is a non-negative number corresponding to the number of elements in argv. The environment calculates this automatically: you don’t have to provide the number of elements in argc. The second argument, argv, is an array of pointers to null-terminated strings that corresponds to an argument passed in from the execution environment.

The third overload : is an extension of the second overload : it accepts an arbitrary number of additional implementation parameters. This way, the target platform can offer some additional arguments to the program. Implementation parameters aren’t common in modern desktop environments.

Usually, an operating system passes the full path to the program’s executable as the first command line argument. This behavior depends on your operating environment. On macOS, Linux, and Windows, the executable’s path is the first argument. The format of this path depends on the operating system. (Chapter 17 discusses filesystems in depth.)

Exploring Program Parameters

Let’s build a program to explore how the operating system passes parameters to your program. Listing 9-29 prints the number of command line arguments and then prints the index and value of the arguments on each line.

#include <cstdio>
#include <cstdint>

int main(int argc, char** argv) { 
  printf("Arguments: %d
", argc); 
  for(size_t i{}; i<argc; i++) {
    printf("%zd: %s
", i, argv[i]); 
  }
}

Listing 9-29: A program that prints the command line arguments. Compile this program as list_929.

You declare main with the argc/argv overload, which makes command line parameters available to your program . First, you print the number of command line arguments via argc . Then you loop through each argument, printing its index and its value .

Let’s look at some sample output (on Windows 10 x64). Here is one program invocation:

$ list_929 
Arguments: 1 
0: list_929.exe 

Here, you provide no additional command line arguments aside from the name of the program, list_929 . (Depending on how you compiled the listing, you should replace this with the name of your executable.) On a Windows 10 x64 machine, the result is that your program receives a single argument , the name of the executable .

And here is another invocation:

$ list_929 Violence is the last refuge of the incompetent. 
Arguments: 9
0: list_929.exe
1: Violence
2: is
3: the
4: last
5: refuge
6: of
7: the
8: incompetent.

Here, you provide additional program arguments: Violence is the last refuge of the incompetent. . You can see from the output that Windows has split the command line by spaces, resulting in a total of nine arguments.

In major desktop operating systems, you can force the operating system to treat such a phrase as a single argument by enclosing it within quotes, as in the following:

$ list_929 "Violence is the last refuge of the incompetent."
Arguments: 2
0: list_929.exe
1: Violence is the last refuge of the incompetent.

A More Involved Example

Now that you know how to process command line input, let’s consider a more involved example. A histogram is an illustration that shows a distribution’s relative frequency. Let’s build a program that computes a histogram of the letter distribution of the command line arguments.

Start with two helper functions that determine whether a given char is an uppercase letter or a lowercase letter:

constexpr char pos_A{ 65 }, pos_Z{ 90 }, pos_a{ 97 }, pos_z{ 122 };
constexpr bool within_AZ(char x) { return pos_A <= x && pos_Z >= x; } 
constexpr bool within_az(char x) { return pos_a <= x && pos_z >= x; } 

The pos_A, pos_Z, pos_a, and pos_z constants contain the ASCII values of the letters A, Z, a, and z respectively (refer to the ASCII chart in Table 2-4). The within_AZ function determines whether some char x is an uppercase letter by determining whether its value is between pos_A and pos_Z inclusive . The within_az function does the same for lowercase letters .

Now that you have some elements for processing ASCII data from the command line, let’s build an AlphaHistogram class that can ingest command line elements and store character frequencies, as shown in Listing 9-30.

struct AlphaHistogram {
  void ingest(const char* x); 
  void print() const; 
private:
  size_t counts[26]{}; 
};

Listing 9-30: An AlphaHistogram that ingests command line elements

An AlphaHistogram will store the frequency of each letter in the counts array . This array initializes to zero whenever an AlphaHistogram is constructed. The ingest method will take a null-terminated string and update counts appropriately . Then the print method will display the histogram information stored in counts .

First, consider the implementation of ingest in Listing 9-31.

void AlphaHistogram::ingest(const char* x) {
  size_t index{}; 
  while(const auto c = x[index]) { 
    if (within_AZ(c)) counts[c - pos_A]++; 
    else if (within_az(c)) counts[c - pos_a]++; 
    index++; 
  }
}

Listing 9-31: An implementation of the ingest method

Because x is a null-terminated string, you don’t know its length ahead of time. So, you initialize an index variable and use a while loop to extract a single char c at a time . This loop will terminate if c is null, which is the end of the string. Within the loop, you use the within_AZ helper function to determine whether c is an uppercase letter . If it is, you subtract pos_A from c. This normalizes an uppercase letter to the interval 0 to 25 to correspond with counts. You do the same check for lowercase letters using the within_az helper function , and you update counts in case c is lowercase. If c is neither lowercase nor uppercase, counts is unaffected. Finally, you increment index before continuing to loop .

Now, consider how to print counts, as shown in Listing 9-32.

void AlphaHistogram::print() const {
  for(auto index{ pos_A }; index <= pos_Z; index++) { 
    printf("%c: ", index); 
    auto n_asterisks = counts[index - pos_A]; 
    while (n_asterisks--) printf("*"); 
    printf("
"); 
  }
}

Listing 9-32: An implementation of the print method

To print the histogram, you loop over each letter from A to Z . Within the loop, you first print the index letter , and then determine how many asterisks to print by extracting the correct letter out of counts . You print the correct number of asterisks using a while loop , and then you print a terminating newline .

Listing 9-33 shows AlphaHistogram in action.

#include <cstdio>
#include <cstdint>

constexpr char pos_A{ 65 }, pos_Z{ 90 }, pos_a{ 97 }, pos_z{ 122 };
constexpr bool within_AZ(char x) { return pos_A <= x && pos_Z >= x; }
constexpr bool within_az(char x) { return pos_a <= x && pos_z >= x; }

struct AlphaHistogram {
  --snip--
};

int main(int argc, char** argv) {
  AlphaHistogram hist;
  for(size_t i{ 1 }; i<argc; i++) { 
    hist.ingest(argv[i]); 
  }
  hist.print(); 
}
--------------------------------------------------------------------------
$ list_933 The quick brown fox jumps over the lazy dog
A: *
B: *
C: *
D: *
E: ***
F: *
G: *
H: **
I: *
J: *
K: *
L: *
M: *
N: *
O: ****
P: *
Q: *
R: **
S: *
T: **
U: **
V: *
W: *
X: *
Y: *
Z: *

Listing 9-33: A program illustrating AlphaHistogram

You iterate over each command line argument after the program name , passing each into the ingest method of your AlphaHistogram object . Once you’ve ingested them all, you print the histogram . Each line corresponds to a letter, and the asterisks show the absolute frequency of the corresponding letter. As you can see, the phrase The quick brown fox jumps over the lazy dog contains each letter in the English alphabet.

Exit Status

The main function can return an int corresponding to the exit status of the program. What the values represent is environment defined. On modern desktop systems, for example, a zero return value corresponds with a successful program execution. If no return statement is explicitly given, an implicit return 0 is added by the compiler.

Summary

This chapter took a deeper look at functions, including how to declare and define them, how to use the myriad keywords available to you to modify function behavior, how to specify return types, how overload resolution works, and how to take a variable number of arguments. After a discussion of how you take pointers to functions, you explored lambda expressions and their relationship to function objects. Then you learned about the entry point for your programs, the main function, and how to take command line arguments.

EXERCISES

9-1. Implement a fold function template with the following prototype:

template <typename Fn, typename In, typename Out>
constexpr Out fold(Fn function, In* input, size_t length, Out initial);

For example, your implementation must support the following usage:

int main() {
  int data[]{ 100, 200, 300, 400, 500 };
  size_t data_len = 5;
  auto sum = fold([](auto x, auto y) { return x + y; }, data, data_len,
0);
  print("Sum: %d
", sum);
}

The value of sum should be 1,500. Use fold to calculate the following quantities: the maximum, the minimum, and the number of elements greater than 200.

9-2. Implement a program that accepts an arbitrary number of command line arguments, counts the length in characters of each argument, and prints a histogram of the argument length distribution.

9-3. Implement an all function with the following prototype:

template <typename Fn, typename In, typename Out>
constexpr bool all(Fn function, In* input, size_t length);

The Fn function type is a predicate that supports bool operator()(In). Your all function must test whether function returns true for every element of input. If it does, return true. Otherwise, return false.

For example, your implementation must support the following usage:

int main() {
  int data[]{ 100, 200, 300, 400, 500 };
  size_t data_len = 5;
  auto all_gt100 = all([](auto x) { return x > 100; }, data, data_len);
  if(all_gt100) printf("All elements greater than 100.
");
}

FURTHER READING

  • Functional Programming in C++: How to Improve Your C++ Programs Using Functional Techniques by Ivan Čukić (Manning, 2019)
  • Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin (Pearson Education, 2009
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset