Other functions
The rules in this section advise on when to use lambdas and compare va_arg with fold expressions.
Lambdas
This rule states the use case for lambdas. This immediately raises the question, When do you have to use a lambda or a function? Here are two obvious reasons.
If your callable has to capture local variables or is declared in a local scope, you have to use a lambda function.
If your callable should support overloading, use a function.
Now I want to present my crucial arguments for lambdas that are often ignored.
Expressiveness
“Explicit is better than implicit.” This meta-rule from Python (PEP 20—The Zen of Python) also applies to C++. It means that your code should explicitly express its intent (see rule “P.1: Express ideas directly in code”). Of course, this holds true in particular for lambdas.
std::vector<std::string> myStrVec = {"523345", "4336893456", "7234", "564", "199", "433", "2435345"}; std::sort(myStrVec.begin(), myStrVec.end(), [](const std::string& f, const std::string& s) { return f.size() < s.size(); } );
Compare this lambda with the function lessLength, which is subsequently used.
std::vector<std::string> myStrVec = {"523345", "4336893456", "7234", "564", "199", "433", "2435345"}; bool lessLength(const std::string& f, const std::string& s) { return f.size() < s.size(); } std::sort(myStrVec.begin(), myStrVec.end(), lessLength);
Both the lambda and the function provide the same order predicate for the sort algorithm. Imagine that your coworker named the function foo. This means you have no idea what the function is supposed to do. As a consequence, you have to document the function.
// sorts the vector ascending, based on the length of its strings std::sort(myStrVec.begin(), myStrVec.end(), foo);
Further, you have to hope that your coworker did it right. If you don’t trust them, you have to analyze the implementation. Maybe that’s not possible because you have the declaration of the function. With a lambda, your coworker cannot fool you. The code is the truth. Let me put it more provocatively: Your code should be so expressive that it does not require documentation.
and
Both rules are strongly related, and they boil down to the following observation: A lambda should operate only on valid data. When the lambda captures the data by copy, the data is by definition valid. When the lambda captures data by reference, the lifetime of the data must outlive the lifetime of the lambda. The previous example with a reference to a local showed different results of a lambda referring to invalid data.
Sometimes the issue is not so easy to catch.
int main() { std::string str{"C++11"}; std::thread thr([&str]{ std::cout << str << '\n'; }); thr.detach(); }
Okay, I hear you say, “That is easy.” The lambda expression used in the created thread thr captures the variable str by reference. Afterward, thr is detached from the lifetime of its creator, which is the main thread. Therefore, there is no guarantee that the created thread thr uses a valid string str because the lifetime of str is bound to the lifetime of the main thread. Here is a straightforward way to fix the issue. Capture str by copy:
int main() { std::string str{"C++11"}; std::thread thr([str]{ std::cout << str << '\n'; }); thr.detach(); }
Problem solved? No! The crucial question is, Who is the owner of std::cout? std::cout’s lifetime is bound to the lifetime of the process. This means that the thread thr may be gone before std::cout prints C++11 onscreen. The way to fix this problem is to join the thread thr. In this case, the creator waits until the created is done, and therefore, capturing by reference is also fine.
int main() { std::string str{"C++11"}; std::thread thr([&str]{ std::cout << str << '\n'; }); thr.join(); }
If you need to invoke a function with a different number of arguments, prefer default arguments over overloading if possible. Therefore, you follow the DRY principle (don’t repeat yourself).
void print(const string& s, format f = {});
The equivalent functionality with overloading requires two functions:
void print(const string& s); // use default format void print(const string& s, format f);
The title of this rule is too short. Use variadic templates instead of va_arg arguments when your function should accept an arbitrary number of arguments.
Variadic functions are functions such as std::printf that can take an arbitrary number of arguments. The issue is that you have to assume that the correct types were passed. Of course, this assumption is very error prone and relies on the discipline of the programmer.
To understand the implicit danger of variadic functions, here is a small example.
// vararg.cpp #include <iostream> #include <cstdarg> int sum(int num, ... ) { int sum = 0; va_list argPointer; va_start(argPointer, num ); for( int i = 0; i < num; i++ ) sum += va_arg(argPointer, int ); va_end(argPointer); return sum; } int main() { std::cout << "sum(1, 5): " << sum(1, 5) << '\n'; std::cout << "sum(3, 1, 2, 3): " << sum(3, 1, 2, 3) << '\n'; std::cout << "sum(3, 1, 2, 3, 4): " << sum(3, 1, 2, 3, 4) << '\n'; // (1) std::cout << "sum(3, 1, 2, 3.5): " << sum(3, 1, 2, 3.5) << '\n'; // (2) }
sum is a variadic function. Its first argument is the number of arguments that should be summed up. The following background information about va_arg macros helps with understanding the code.
va_list: holds the necessary information for the following macros
va_start: enables access to the variadic function arguments
va_arg: accesses the next variadic function argument
va_end: ends the access of the variadic function arguments
For more information, read cppreference.com about variadic functions.
In (1) and (2), I had a bad day. First, the number of the arguments num is wrong; second, I provided a double instead of an int. The output shows both issues. The last element in (1) is missing, and the double is interpreted as int (2). See Figure 4.7.
Figure 4.7 Summation with va_arg
These issues can be easily overcome with fold expressions in C++17. In contrast to va_args, fold expressions automatically deduce the number and the type of their arguments.
// foldExpressions.cpp #include <iostream> template<class ... Args> auto sum(Args ... args) { return (... + args); } int main() { std::cout << "sum(5): " << sum(5) << '\n'; std::cout << "sum(1, 2, 3): " << sum(1, 2, 3) << '\n'; std::cout << "sum(1, 2, 3, 4): " << sum(1, 2, 3, 4) << '\n'; std::cout << "sum(1, 2, 3.5): " << sum(1, 2, 3.5) << '\n'; }
The function sum may look scary to you. It requires at least one argument and uses C++11 variadic templates. These are templates that can accept an arbitrary number of arguments. The arbitrary number is held by a so-called parameter pack denoted by an ellipsis (. . .). Additionally, with C++17, you can directly reduce a parameter pack with a binary operator. This addition, based on variadic templates, is called fold expressions. In the case of the sum function, the binary + operator (...+ args) is applied. If you want to know more about fold expressions in C++17, details are at https://www.modernescpp.com/index.php/fold-expressions.
The output of the program is as expected (see Figure 4.8).
Figure 4.8 Summation with fold expressions