Книга: C++23 STL Cookbook
Назад: Chapter 4: STL Compatible Iterators
Дальше: Chapter 6: STL Algorithms

5

Lambda Expressions

The C++11 standard introduced lambda expressions (sometimes called lambda functions , or simply lambdas ). This feature allows an anonymous function to be used in the context of an expression. Lambdas may be used in function calls, containers, variables, and other expression contexts. It may sound innocuous, but it's remarkably useful.

In this chapter, we will cover the use of lambdas with the STL, in the following recipes:

  • Lambdas and closures
  • Use lambdas for scoped reusable code
  • Use lambdas as predicates with algorithms
  • Create a polymorphic wrapper with std::function
  • Concatenate lambdas with recursion
  • Combine predicates with a logical conjunction
  • Pass common parameters to multiple lambdas
  • Create a simple jump table with mapped lambdas

Let's start with a brief review of lambda expressions.

Lambda expressions

A lambda is essentially an anonymous function as a literal expression:

auto la = []{ return "Hello"; };

The variable la may now be used as if it were a function:

println("{}", la());

It can be passed to another function:

f(la);

It can be passed to another lambda:

const auto la = []{ return "Hello"; }; const auto lb = [](auto a){ return a(); }; println("{}", lb(la));

Output:

Hello

Or it can be passed anonymously (as a literal):

const auto lb = [](auto a){ return a(); }; println("{}", lb([]{ return "Hello"; }));

Closures

The term closure is often applied to any anonymous function. Strictly speaking, a closure is a function that allows the use of symbols outside its own lexical scope.

You may have noticed the square brackets in the definition of a lambda:

auto la = []{ return "Hello"; };

The square brackets are used to specify a list of captures . Captures are outside variables that are accessible from within the scope of the lambda body. If I try to use an outside variable without listing it as a capture, I'll get a compilation error:

const char * greeting{ "Hello\n" }; const auto la = []{ return greeting; }; println("{}", la());

When I try to compile this with GCC, I get the following error:

In lambda function: error: 'greeting' is not captured

This is because the body of the lambda has its own lexical scope, and the greeting variable is outside of that scope.

I can specify the greeting variable in a capture . This allows the variable into the scope of the lambda:

const char * greeting{ "Hello\n" }; const auto la = [greeting]{ return greeting; }; println("{}", la());

Now it compiles and runs as expected:

$ ./working Hello

This ability to capture variables outside its own scope is what makes a lambda a closure . People use the term in different ways, and that's fine, so long as we can understand each other. Still, it's good to know what the term means.

Lambda expressions allow us to write good, clean generic code. They allow the use of functional programming patterns, where we can use lambdas as functional parameters to algorithms and even other lambdas.

Use lambdas for scoped, reusable code

Lambda expressions can be defined and stored for later use. They can be passed as parameters, stored in data structures, and called in different contexts with different parameters. They are as flexible as functions, but with the mobility of data.

How to do it

We'll start with a simple program to test various configurations of lambda expressions:

  • First, we define a main() function that we can use to experiment with lambdas:
    int main() {     ... // code goes here }
  • Inside the main() function, we'll declare a couple of lambdas

    The basic definition of a lambda requires a pair of square brackets and a block of code in curly brackets:

    auto one = [](){ return "one"; }; auto two = []{ return "two"; };

    Notice that the first example one includes parentheses after the square brackets, and the second example two does not. The empty parameter parentheses are often included but are not required. The return type is inferred by the compiler.

  • I can call these functions in any context that will take a C-string:
    println("{}", one()); println("{}", two());
  • In many cases, the compiler can determine the return type from automatic type deduction . Otherwise, you can specify the return type with the - > operator:
    auto one = [] -> const char * { return "one"; }; auto two = [] -> auto { return "two"; };

    Lambdas use trailing return type syntax. This consists of the - > operator followed by the type specification . If the return type is not specified, it is considered auto .

    Let's define a lambda to print out the values from our other lambdas:

    auto p = [](auto v){ println("{}", v()); };

    The p() lambda expects a lambda (or function) as its parameter v and calls it in its function body.

    The auto type parameter makes this lambda an abbreviated template . Before C++20, this was the only way to template a lambda. Beginning with C++20, you may specify template parameters after the capture brackets. This is the equivalent with a template parameter:

    auto p = []<typename T>(T v){ println("{}", v()); };

    The abbreviated auto version is simpler and more common. It works well for most purposes.

  • Now we can pass an anonymous lambda to this lambda in the function call:
    p([]{ return "lambda call lambda"; });

    The output is:

    lambda call lambda
  • If we need to pass parameters to an anonymous lambda, we can put them in parentheses after the lambda expression:
    println("{}",     [](auto l, auto r){ return l + r; }(47, 73));

    The function parameters, 47 and 73 , are passed to the anonymous lambda in parentheses after the function body.

  • You can access variables from the outside scope of the lambda by including them as captures in the square brackets:
    int num {1}; p([num]{ return num; });
  • Or you can capture them by reference:
    int num {}; autoinc=[&num]{ return ++num; }; for (size_t i {}; i < 5; ++i) {     print("{} ", inc()); } print("\n");

    The output is as follows:

    1 2 3 4 5

    Capture by reference allows you to modify a captured variable.

  • You can also define a local capture variable that maintains its state:
    auto counter = [n = 0]() mutable { return ++n; }; for (size_t i {}; i < 5; ++i) {     print("{} ", counter()); } print("\n");

    Output:

    1, 2, 3, 4, 5

    The mutable specifier allows the lambda to modify its captures. Lambda captures default to const -qualified.

    As with the trailing return type, any specifier requires the parameter parentheses.

  • The lambda supports two types of default capture :
    int a = 47; int b = 73; auto l1 = []{ return a + b; };

    If I try to compile this code, I get an error that includes:

    note: the lambda has no capture-default

    One type of default capture is indicated by an equal sign:

    auto l1 = [=]{ return a + b; };

    This will capture all the symbols in the lambda's scope. The equal sign performs capture by copy . It will capture a copy of the objects as if they were copied with an assignment operator.

    The other default capture uses an ampersand for capture by reference :

    autol1=[&]{returna+b; };

    This is a default capture that captures by reference.

    The default captures only use symbols when they are referenced, so they're not as messy as they may look. That said, I recommend explicit captures where possible as they generally improve readability.

Prior to C++23, whenever a return type , qualifier , modifier , or exception attribute was included, the parentheses were required even without parameters:

auto one = []() ‑> const char * { return "one"; };

As of C++23, empty parentheses are no longer required in any circumstances:

auto one = [] ‑> const char * { return "one"; };

How it works

The syntax of a lambda expression is as follows:

Figure 5.1 – Syntax of the lambda expression

Figure 5.1: Syntax of a lambda expression

The only required parts of a lambda expression are the capture list and the body, which may be empty:

[]{}

This is the minimal lambda expression. It captures nothing and does nothing.

Let's consider each of the parts.

Capture list

The capture-list specifies what we capture, if anything. It cannot be omitted, but it may be empty. We can use [=] to capture all variables by copy or [&] to capture all variables by reference , within the scope of the lambda.

You may capture individual variables by listing them in the brackets:

[a, b]{ return a + b; }

The specified captures default to capture by copy . You may capture by reference with the reference operator:

[&a, &b]{ return a + b; }

When you capture by reference, you may modify the referenced variable.

You cannot capture object members directly. You may capture this or *this to dereference class members.

Parameters

As with a function, parameters are specified in parentheses:

[](int a, int b){ return a + b };

If there are no parameters, the parentheses are not required. Prior to C++23, the parentheses were always required if a return type , qualifier , modifier , or exception attribute , was included:

[]() -> int { return 47 + 73 };

As of C++23, the parentheses are optional if empty.

The mutable modifier (optional)

A lambda expression defaults to const -qualified unless you specify the mutable modifier. This allows the lambda to be used in const contexts. It also means that any captured-by-copy variables cannot be modified. For example:

[a]{ return ++a; };

This will fail to compile with an error message like this:

In lambda function: error: increment of read-only variable 'a'

With the mutable modifier, the lambda is no longer const -qualified and the captured variable may be changed:

[a]() mutable { return ++a; };

The constexpr specifier (optional)

You may use constexpr to explicitly specify that you want your lambda to be considered a constant expression . This means that it may be evaluated at compile time:

constexpr int z {42}; constexpr auto x = [z] constexpr { return z; };

If the lambda meets the requirements, it may be considered constexpr even without the specifier. In practice, the compiler should take care of this for you and you should never need to use the specifier.

The noexcept attribute (optional)

You can use the noexcept specifier to declare that your lambda does not throw any exceptions:

auto x = [] noexcept { return 42; };

The trailing return type (optional)

By default, the lambda return type is deduced from the return statement, as if the return type was auto . You may optionally specify a trailing return type with the ‑> operator:

[](int a, int b) -> long { return a + b; };

In this example, the return type is long . It would have otherwise been deduced as int without the specified return type.

Compiler c ompatibility

At the time of this writing, current versions of Microsoft Visual C++ have yet to implement the C++23 lambda syntax. Mostly, this means that parameter parentheses are required according to C++20 rules. Under current versions (both release and preview), this will not compile:

auto one = [] ‑> const char * { return "one"; };

It requires empty parentheses, as in C++20:

auto one = []() ‑> const char * { return "one"; };

If you're using the Microsoft compiler, please be aware of this limitation.

Use lambdas as predicates with algorithms

Some functions in the algorithm library require the use of a predicate function . A predicate is a function that tests a condition and returns a Boolean true / false response.

For example, the count_if() function from the algorithm library returns the number of elements in a sequence which match a condition. The count_if() function has this signature:

size_t count_if(InputIt first, InputIt last, UnaryPred p);

The condition is specified by the predicate function p . The predicate may be any callable object, including a function pointer, functor (a function object), or a lambda. In modern C++, it's most common to use a lambda for this. A predicate function may be unary or binary . A unary predicate takes one argument and a binary predicate takes two arguments. In this recipe, we'll consider both unary and binary predicates using lambdas.

How to do it

For this recipe, we will experiment with the count_if() and sort() algorithms using different types of predicates:

  • First, let's create a function for use as a predicate with count_if() . A predicate takes a certain number of arguments and returns a bool . A predicate for count_if() takes one argument:
    bool is_div4(const int i) {     return i % 4 == 0; }

    This predicate checks an int to see if it's divisible by 4.

  • In main(), we'll define a vector of int values, and use it to test our predicate function with count_if() :
    int main() {     vector<int> v {1, 7, 12, 20, 4, 9, 4, 8};     auto count4 = count_if(v.begin(), v.end(), is_div4);     println("numbers divisible by 4 (is_div4): {}",       count4); }

    The output is as follows:

    numbers divisible by 4 (is_div4): 5

    (The 5 divisible numbers are: 4 , 4 , 8 , 12 , and 20 .)

    The count_if() algorithm uses the predicate function to determine which elements of the sequence to count. It calls the predicate with each element as a parameter, and only counts the element if the predicate returns true .

    In this case, we used a function as a predicate.

  • We could also use a functor as a predicate:
    struct is_div4 {     bool operator()(const int i) const {         return i % 4 == 0;     } };

    The only change here is that we need to use an instance of the class as the predicate:

    auto count4 = count_if(v.begin(), v.end(), is_div4());

    The advantages of a functor are that it can carry context, and it can access class and instance variables. This was the common way to use predicates before C++11 introduced lambda expressions.

  • With a lambda expression, we have the best of both worlds: the simplicity of a function and the power of a functor. We can use a lambda as a variable:
    auto is_div4 = [](int i){ return i % 4 == 0; }; auto count4 = count_if(v.begin(), v.end(), is_div4);

    Or we can use an anonymous lambda:

    auto count = count_if(v.begin(), v.end(),     [](const int i) { return i % 4 == 0; });
  • We can take advantage of the lambda capture by wrapping the lambda in a function and using that function context to produce the same lambda with different parameters:
    auto is_div_by(const int divisor) {     return [divisor] (const int i)         { return i % divisor == 0; }; }

    This function returns a predicate lambda with the divisor from the capture context.

    We can then use that predicate with count_if() :

    for (int i : { 3, 4, 5 }) {     auto pred = is_div_by(i);     auto count = count_if(v.begin(), v.end(), pred);     println("numbers divisible by {}: {} (is_div_by)", i,       count); }

    Each call to is_div_by() returns a predicate with a different divisor from i . Now we get this output:

    numbers divisible by 3 (is_div_by): 2 numbers divisible by 4 (is_div_by): 5 numbers divisible by 5 (is_div_by): 2
  • A binary predicate is simply a predicate that takes two arguments. For example, the ranges::sort() algorithm may use an optional predicate as a custom comparator:
    auto comp = [](int a, int b) { return a < b; }; println("unsorted: {}", v); ranges::sort(v, comp); println("sorted: {}", v);

    Here we use a lambda for a custom comparator predicate that sorts our vector in descending order. The arguments are passed to the predicate by the sort() function which uses the predicate to compare values in its sorting algorithm.

    This gives us the output:

    unsorted: [1, 7, 12, 20, 4, 9, 4, 8] sorted: [20, 12, 9, 8, 7, 4, 4, 1]

How it works

The type of a function pointer is represented as a pointer followed by the function call () operator:

void (*)()

You can declare a function pointer and initialize it with the name of an existing function:

void (*fp)() = func;

This declaration creates a variable named fp with the type of void(*)() , a pointer to a function that returns void . Once declared, a function pointer may be dereferenced and used as if it were the function itself:

func();  // do the func thing

A lambda expression has the same type as a function pointer:

void (*fp)() = []{ return 42; };

This means that wherever you use a function pointer with a certain signature, you may also use a lambda with the same signature. This allows function pointers, functors, and lambdas to work interchangeably:

bool (*fp)(int) = is_div4; bool (*fp)(int) = [](int i){ return i % 4 == 0; };

Because of this interchangeability, an algorithm such as count_if()or sort() may accept a function, functor, or lambda where it expects a predicate with a particular function signature.

This applies to any algorithm that uses a predicate.

Create a polymorphic wrapper with std::function

The std::function class is a thin polymorphic wrapper for functions. It can store, copy, and invoke references to any function, lambda expression, or other functor. It's polymorphic in that it allows you to store functions and lambdas with different signatures in the same container, while maintaining the context of lambda captures. It's useful in places where you would like to store a reference to a function or lambda.

How to do it

This recipe uses the std::function class to store different specializations of a lambda in a vector :

  • This recipe is contained in the main() function, where we start by declaring three containers of different types:
    int main() {     deque<int> d;     list<int> l;     vector<int> v;

    These containers, deque , list , and vector , will be referenced by a template lambda.

  • We declare a simple print_c lambda function for printing out the containers:
    auto print_c = [] (const auto& c) {     for (const auto i : c) print("{} ", i);     print("\n"); };

    The print_c lambda function takes a container parameter and uses a for loop to print its elements.

  • Now we declare another lambda that returns an anonymous lambda:
    auto push_c = [] (auto& container) {     return [&container] (const auto value) {         container.push_back(value);     }; };

    This is a lambda that returns another lambda. The push_c lambda takes a reference to a container, which is then captured by the anonymous lambda. The anonymous lambda calls the push_back() member on the captured container. The return value from push_c is the anonymous lambda.

  • Now we declare a vector of std::function elements, and populate it with three instances of push_c() :
    const vector<function<void(int)>>     consumers {push_c(d), push_c(l), push_c(v)};

    The vector object is named consumers . Its template parameter specifies a signature using the function class. The template parameter on the function class is a generic function that takes one parameter. The type of the parameter in the function signature uses int as a generic placeholder.

    Each of the elements in the initializer list is a function call to the push_c lambda. push_c returns an instance of the anonymous lambda, which gets stored in the vector via the function wrapper. The push_c lambda is called with the three containers, d , l , and v , a deque , a list , and a vector respectively. The containers are passed as captures with the anonymous lambda.

  • Now we loop through the consumers vector and call each of the lambda elements 10 times, populating the three containers with integers 0 9 in each container:
    for (auto &c : consumers) {     for (int i{0}; i < 10; ++i) {         c(i);     } }
  • Now our three containers, the deque , list , and vector , should all be populated with integers. Let's print them out:
    print_c(d); print_c(l); print_c(v);

    Our output should be:

    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

How it works

Lambdas are often used with indirection, and this recipe is a good example of that. For example, the push_c lambda returns an anonymous lambda:

auto push_c = [] (auto& container) {     return [&container] (const auto value) {         container.push_back(value);     }; };

This anonymous lambda gets stored in the vector of function objects:

const vector<function<void(int)>>     consumers {push_c(d), push_c(l), push_c(v)};

This defines the consumers container. It is initialized with three elements, where each element is the lambda returned by a call to push_c . The anonymous lambda gets stored in the vector, not the push_c lambda.

The vector definition uses the function class as the type of the elements. The function constructor takes any callable object and stores its reference as the function target:

template< class F > function(F&&f);

When its function call () operator is invoked, the function object calls the target function with the intended parameters:

for (auto& c : consumers) {     for (int i{0}; i < 10; ++i) {         c(i);     } }

This calls each of the anonymous lambdas, as stored in the consumers container, 10 times, thus populating the d , l , and v containers.

There's more…

The nature of the std::function class makes it useful for many purposes. You can think of it as a polymorphic function container. It can store a standalone function:

void hello() {     println("hello"); }  int main() {     const function<void(void)> h = hello;     h(); }

It can store a member function, using std::bind to bind function parameters:

struct hello {     void greeting() const { println("Hello Human"); } }; int main() {     hello human{};     const function<void(void)> h =         std::bind(&hello::greeting,&human);     h(); }

Or it can store any executable object:

struct hello {     void operator()() const { println("Hello Human"); } }; int main() {     const function<void(void)> h = hello();     h(); }

The output is as follows:

Hello Human

Concatenate lambdas with recursion

You can stack lambdas so that the output of one is the input of the next, using a simple recursive function. This creates a simple way to build one function upon another, allowing the lambdas to serve as modular building blocks.

How to do it

This is a short and simple recipe that uses one recursive function to do most of the work:

  • We'll start by defining the concatenation function concat() :
    template <typename T, typename ...Ts>         auto concat(const T t, const Ts ...ts) {     if constexpr (sizeof...(ts) > 0) {         return [=](const auto ...parameters) {             return t(concat(ts...)(parameters...));         };     } else  {         return t;     } }

    This function returns an anonymous lambda, which in turn calls the function again, until the parameter pack is exhausted.

  • In the main() function, we create a couple of lambdas and call the concat() function with them:
    int main() {     auto twice = [](const auto i) { return i * 2; };     auto thrice = [](const auto i) { return i * 3; };      auto combined = concat(thrice, twice,         std::plus<int>{});     println("{}", combined(2, 3)); }

    The concat() function is called with three parameters: two lambdas, and the std::plus() function.

    As the recursion unravels, the functions are called right-to-left, starting with plus() . The plus() function takes two arguments and returns the sum. The return value from plus() is passed to twice() , and its return value is passed to thrice() . The result is then printed to the console with println() .

    Output:

    30

How it works

Let's take a closer look at the concat() function. It is simple, but it may be confusing due to the recursion and the indirection of the returned lambda:

template <typename T, typename ...Ts> auto concat(const T t, const Ts ...ts) {     if constexpr (sizeof...(ts) > 0) {         return [=](const auto ...parameters) {             return t(concat(ts...)(parameters...));         };     } else  {         return t;     } }

The concat() function is called with a template parameter pack . The sizeof... operator (with ellipsis) returns the number of elements in the parameter pack. This is used to test for the end of the recursion. Because the sizeof... operator returns a constant that must be evaluated at compile time, the if() expression must use the constexpr modifier.

The concat() function returns a lambda. The lambda recursively calls the concat() function in the return expression. Because the first argument of concat() is not part of the parameter pack, each recursive call peels off the first element of the pack.

The outer return statement returns the lambda. The inner return calls the function that was passed to concat() and returns its value.

Feel free to take this apart and study it. There's value in this technique.

Combine predicates with logical conjunction

This example wraps a lambda in a function to create a custom conjunction for use with an algorithm predicate.

How to do it

The copy_if() algorithm requires a predicate that takes one parameter. In this recipe, we create a predicate lambda from three other lambdas:

  • First, we'll write the combine() function. This function returns a lambda for use with the copy_if() algorithm:
    template <typename F, typename A, typename B> auto combine(F binary_func, A a, B b) {     return [=](auto param) {         return binary_func(a(param), b(param));     }; }

    The combine() function takes three function parameters – a binary conjunction and two predicates – and returns a lambda that calls the conjunction with the two predicates.

  • In the main() function, we create the lambdas for use with combine() :
    auto begins_with = [](const string &s)     { return s.find("a") == 0; }; auto ends_with = [](const string &s)     { return s.rfind("b") == s.length() - 1; }; auto bool_and = [](const auto& l, const auto& r)     { returnl&&r };

    The begins_with and ends_with lambdas are simple filter predicates to find strings that begin with 'a' and end with 'b' , respectively. The bool_and lambda is the conjunction.

  • Now we can call the copy_if algorithm with combine() :
    std::copy_if(istream_iterator<string>{cin}, {},              ostream_iterator<string>{cout, " "},              combine(bool_and, begins_with, ends_with));  print("\n");

    The combine() function returns a lambda that combines the two predicates with the conjunction.

    The output looks like the following:

    $ echo aabb bbaa foo bar abazb | ./conjunction aabb abazb

How it works

The std::copy_if() algorithm requires a predicate function that takes one parameter, but our conjunction requires two parameters, each of which requires one parameter. We resolve this with a function that returns a lambda specifically for this context:

template <typename F, typename A, typename B> auto combine(F binary_func, A a, B b) {     return [=](auto param) {         return binary_func(a(param), b(param));     }; }

The combine() function creates a lambda from three parameters, each of which is a function. The returned lambda takes the one parameter that's required of copy_if() 's predicate function. Now we can call copy_if() with the combine() function:

std::copy_if(istream_iterator<string>{cin}, {},              ostream_iterator<string>{cout, " "},              combine(bool_and, begins_with, ends_with));

This passes the combined lambda to the algorithm so it can operate within that context.

Pass common parameters to multiple lambdas

You can easily create multiple instances of a lambda with different capture values by wrapping the lambda in a function. This allows you to call different versions of a lambda with the same input.

How to do it

This is a simple example of a lambda that wraps a value in different types of braces:

  • We'll start by creating the wrapper function braces() :
    auto braces (const char a, const char b) {     return [a, b](const auto v) {         print("{}{}{} ", a, v, b);     }; }

    The braces() function wraps a lambda that returns a three-value string, where the first and last values are characters passed to the lambda as captures, and the middle value is passed as a parameter.

  • In the main() function, we use braces() to create four lambdas, using four different sets of braces:
    auto a = braces('(', ')'); auto b = braces('[', ']'); auto c = braces('{', '}'); auto d = braces('|', '|');
  • Now we can call our lambdas from a simple for() loop:
    for (auto i : {1, 2, 3, 4, 5}) {     for (auto x : {a, b, c, d}) x(i);     print("\n"); }

    In this pair of nested for loops, the outer loop counts from 1 to 5, passing an integer to the inner loop. The inner loop calls the lambdas with the different sets of braces.

    Both loops use an initializer_list as the container in a range-based for loop. This is a convenient technique for looping through a small set of values.

  • The output from our program looks like this:
    (1) [1] {1} |1| (2) [2] {2} |2| (3) [3] {3} |3| (4) [4] {4} |4| (5) [5] {5} |5|

    The output shows each of the integers, in each combination of braces.

How it works

This is a simple example of how to use a wrapper for a lambda. The braces() function constructs a lambda using the braces passed to it:

auto braces (const char a, const char b) {     return [a, b](const auto v) {         print("{}{}{} ", a, v, b);     }; }

By passing the braces() function parameters to the lambda, it can return a lambda with that context. So, each of the assignments in the main function carries those parameters with it:

auto a = braces('(', ')'); auto b = braces('[', ']'); auto c = braces('{', '}'); auto d = braces('|', '|');

When these lambdas are called with a digit, they will return a string with that digit in the corresponding braces.

Create a simple jump table with mapped lambdas

A jump table is a useful pattern when you want to select an action from a user or other input. Jump tables are useful as state machines and are often implemented in if / else or switch structures. In this recipe, we'll build a concise jump table using only an STL map and anonymous lambdas.

How to do it

It's easy to build a simple jump table from a map and lambdas. The map provides simple indexed navigation and the lambda can be stored as payload. Here's how to do it:

  • First, we'll create a simple prompt() function to get input from the console:
    const char prompt(const char * p) {     constexpr size_t BUFLEN {4};     constexpr int NL {'\n'};     char bufin[BUFLEN] {};     int bufchar {};      // fold char to uppercase     auto char_upper = [](char c) ->char {         if (c >= 'a'&&c<= 'z') return c - ('a' - 'A');         else return c;     };      // flush the input buffer     auto flush_stdin = []{         int c {};         while(c!=NL&&c!=EOF)c=getchar();     };      std::print("{} > ", p);     for (size_t i {};           i <BUFLEN&&(bufchar=getchar())!=NL;           ++i) {         bufin[i] = (char) bufchar;     }      if (bufchar != NL) flush_stdin();     if (bufchar == EOF) exit(0);      const char r0 = bufin[0];     const char r1 = bufin[1];      if (r0 == 0 || r0 == NL) return 0;     else if (r1 != 0 && r1 != NL) {         println("Response too long");         return 0;     }     else return char_upper(r0); }

    The C-string parameter is used as a prompt. std::getchar() is called to get input from the user. The response is stored in bufin , checked for length, then if it's one character in length, it's converted to uppercase and returned.

    We use a lambda for the uppercase conversion, and another for flushing the input buffer. Using lambdas for this allows us to keep these functions in the local scope of the prompt() function.

  • We create a jump() function for the jump table. Here, we declare and initialize a map of lambdas:
    const bool jump(const char select) {     using jumpfunc = void(*)();      static const std::map<char, jumpfunc> jumpmap {         { 'A', []{ println("func A"); } },         { 'B', []{ println("func B"); } },         { 'C', []{ println("func C"); } },         { 'D', []{ println("func D"); } },     };        const auto it = jumpmap.find(select);     if (it != jumpmap.end()) it->second();     else {         println("Invalid response");         return false;     }     return true; }

    The map container is loaded with anonymous lambdas for the jump table. These lambdas could easily call other functions or perform simple tasks. We store our map in static storage so it's only allocated once, while keeping it within the limited scope of the jump() function.

    The using alias is for convenience. We're using the function pointer type void(*)() for the lambda payload. If you prefer, you could use std::function() if you need more flexibility or if you just find it more readable. It has very little overhead:

    using jumpfunc = std::function<void()>;

    To select a target, we call find() on the map object, then call the lambda with it‑>second() .

  • In the main() function, we prompt for user input and select an action from the map :
    int main() {     const char* pstr {"What to do? (A/B/C/D/X)"};     for (auto key = prompt(pstr);           key != 'X';           key = prompt(pstr)) {         if (key) jump(key);     }     println("Bye!"); }

    This is how we use the map -based jump table. We loop until 'X' is selected for exit. We call prompt() with a prompt string and jump() with the keypress.

How it works

The map container makes an excellent jump table. It's concise and easy to navigate:

using jumpfunc = void(*)(); map<const char, jumpfunc> jumpmap {     { 'A', []{ cout << "func A\n"; } },     { 'B', []{ cout << "func B\n"; } },     { 'C', []{ cout << "func C\n"; } },     { 'D', []{ cout << "func D\n"; } },     { 'X', []{ cout << "Bye!\n"; } } };

Anonymous lambdas are stored as payload in the map container. The keys are the character responses from the menu of actions.

You can test the validity of a key and select a lambda in one action:

const auto it = jumpmap.find(select); if (it != jumpmap.end()) it->second(); else {     println("Invalid response");     return false; } return true;

This is a simple, elegant solution, where we would have otherwise used awkward branching code.

Subscribe to Deep Engineering

Join thousands of developers and architects who want to understand how software is changing, deepen their expertise, and build systems that last.

Deep Engineering is a weekly expert-led newsletter for experienced practitioners, featuring original analysis, technical interviews, and curated insights on architecture, system design, and modern programming practice.

Scan the QR or visit the link to subscribe for free.

Deep Engineering QR

Назад: Chapter 4: STL Compatible Iterators
Дальше: Chapter 6: STL Algorithms