- Modern C++ Programming Cookbook
- Marius Bancila
- 1285字
- 2021-06-11 18:22:19
Using generic and template lambdas
In the preceding recipe, we saw how to write lambda expressions and use them with standard algorithms. In C++, lambdas are basically syntactic sugar for unnamed function objects, which are classes that implement the call operator. However, just like any other function, this can be implemented generically with templates. C++14 takes advantage of this and introduces generic lambdas that do not need to specify actual types for their parameters and use the auto specifier instead. Though not referred to with this name, generic lambdas are basically lambda templates. They are useful in cases where we want to use the same lambda but with different types of parameter. Moreover, the C++20 standard takes this a step further and supports explicitly defining template lambdas. This helps with some scenarios where generic lambdas are cumbersome.
Getting started
It is recommended that you read the preceding recipe, Using lambdas with standard algorithms, before you continue with this one to familiarize yourself with the fundamentals of lambdas in C++.
How to do it...
In C++14, we can write generic lambdas:
- By using the auto specifier instead of actual types for lambda expression parameters
- When we need to use multiple lambdas that only differ by their parameter types
The following example shows a generic lambda used with the std::accumulate() algorithm, first with a vector of integers and then with a vector of strings:
auto numbers =
std::vector<int>{0, 2, -3, 5, -1, 6, 8, -4, 9};
using namespace std::string_literals;
auto texts =
std::vector<std::string>{"hello"s, " "s, "world"s, "!"s};
auto lsum = [](auto const s, auto const n) {return s + n;};
auto sum = std::accumulate(
std::begin(numbers), std::end(numbers), 0, lsum);
// sum = 22
auto text = std::accumulate(
std::begin(texts), std::end(texts), ""s, lsum);
// sum = "hello world!"s
In C++20, we can write template lambdas:
- By using a template parameter list in angle brackets (such as <template T>) after the capture clause
- When you want to:
- Restrict the use of a generic lambda with only some types, such as a container, or types that satisfy a concept.
- Make sure that two or more arguments of a generic lambda actually do have the same type.
- Retrieve the type of a generic parameter so that, for example, we can create instances of it, invoke static methods, or use its iterator types.
- Perform perfect forwarding in a generic lambda.
The following example shows a template lambda that can be invoked only using an std::vector:
std::vector<int> vi { 1, 1, 2, 3, 5, 8 };
auto tl = []<typename T>(std::vector<T> const& vec)
{
std::cout << std::size(vec) << '\n';
};
tl(vi); // OK, prints 6
tl(42); // error
How it works...
In the first example from the previous section, we defined a named lambda expression; that is, a lambda expression that has its closure assigned to a variable. This variable is then passed as an argument to the std::accumulate() function.
This general algorithm takes the begin and the end iterators, which define a range, an initial value to accumulate over, and a function that is supposed to accumulate each value in the range to the total. This function takes a first parameter representing the currently accumulated value and a second parameter representing the current value to accumulate to the total, and it returns the new accumulated value. Note that I did not use the term add because this can be used for other things than just adding. It can also be used for calculating a product, concatenating, or other operations that aggregate values together.
The two calls to std::accumulate() in this example are almost the same; only the types of the arguments are different:
- In the first call, we pass iterators to a range of integers (from a vector<int>), 0 for the initial sum, and a lambda that adds two integers and returns their sum. This produces a sum of all integers in the range; for this example, it is 22.
- In the second call, we pass iterators to a range of strings (from a vector<string>), an empty string for the initial value, and a lambda that concatenates two strings by adding them together and returning the result. This produces a string that contains all the strings in the range put together one after another; for this example, the result is hello world!.
Though generic lambdas can be defined anonymously in the place where they are called, it does not really make sense because the very purpose of a generic lambda (which is basically, as we mentioned earlier, a lambda expression template) is to be reused, as shown in the example from the How to do it... section.
When defining this lambda expression, when used with multiple calls to std::accumulate(), instead of specifying concrete types for the lambda parameters (such as int or std::string), we used the auto specifier and let the compiler deduce the type. When encountering a lambda expression that has the auto specifier for a parameter type, the compiler generates an unnamed function object that has a call operator template. For the generic lambda expression in this example, the function object would look like this:
struct __lambda_name__
{
template<typename T1, typename T2>
auto operator()(T1 const s, T2 const n) const { return s + n; }
__lambda_name__(const __lambda_name__&) = default;
__lambda_name__(__lambda_name__&&) = default;
__lambda_name__& operator=(const __lambda_name__&) = delete;
~__lambda_name__() = default;
};
The call operator is a template with a type parameter for each parameter in the lambda that was specified with auto. The return type of the call operator is also auto, which means the compiler will deduce it from the type of the returned value. This operator template will be instantiated with the actual types the compiler will identify in the context where the generic lambda is used.
The C++20 template lambdas are an improvement of the C++14 generic lambdas, making some scenarios easier. A typical one was shown in the second example of the previous section, where the use of lambda was restricted with arguments of the type std::vector. Another example is when you want to make sure that two parameters of the lambda have the same type. Prior to C++20, this was difficult to do, but with template lambdas, it is very easy, as shown in the following example:
auto tl = []<typename T>(T x, T y)
{
std::cout << x << ' ' << y << '\n';
};
tl(10, 20); // OK
tl(10, "20"); // error
Another scenario for template lambdas is when you need to know the type of a parameter so that you can create instances of that type or invoke static members of it. With generic lambdas, the solution is as follows:
struct foo
{
static void f() { std::cout << "foo\n"; }
};
auto tl = [](auto x)
{
using T = std::decay_t<decltype(x)>;
T other;
T::f();
};
tl(foo{});
This solution requires the use of std::decay_t and decltype. However, in C++20, the same lambda can be written as follows:
auto tl = []<typename T>(T x)
{
T other;
T::f();
};
A similar situation occurs when we need to do perfect forwarding in a generic lambda, which requires the use of decltype to determine the types of the arguments:
template <typename ...T>
void foo(T&& ... args)
{ /* ... */ }
auto tl = [](auto&& ...args)
{
return foo(std::forward<decltype(args)>(args)...);
};
tl(1, 42.99, "lambda");
With template lambda, we can rewrite it in a simpler way as follows:
auto tl = []<typename ...T>(T && ...args)
{
return foo(std::forward<T>(args)...);
};
As seen in these examples, template lambdas are an improvement of generic lambdas, making it easier to handle the scenarios mentioned in this recipe.
See also
- Using lambdas with standard algorithms to explore the basics of lambda expressions and how you can utilize them with the standard algorithms
- Using auto whenever possible in Chapter 1, Learning Modern Core Language Features, to understand how automatic type deduction works in C++
- Mastering NetBeans
- TensorFlow Lite移動端深度學習
- Architecting the Industrial Internet
- x86匯編語言:從實模式到保護模式(第2版)
- 秒懂設計模式
- D3.js 4.x Data Visualization(Third Edition)
- Learning Zurb Foundation
- Haskell Data Analysis Cookbook
- Python爬蟲、數據分析與可視化:工具詳解與案例實戰
- PHP 7從零基礎到項目實戰
- Django 3.0應用開發詳解
- Android移動應用項目化教程
- Node.js 6.x Blueprints
- Java EE 7 Development with WildFly
- SQL Server 2014 Development Essentials