Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
josuttis_nm_c20_the_complete_guide.pdf
Скачиваний:
48
Добавлен:
27.03.2023
Размер:
5.85 Mб
Скачать

592

Chapter 17: Lambda Extensions

Alternatively, you might think about using a variable template, a technique introduced in C++14. With this, you can make the variable primeNumbers generic instead of making the lambda generic:

template<int Num>

auto primeNumbers = [] () {

std::array<int, Num> primes{};

... // compute and assign first Num prime numbers return primes;

};

...

// initialize array with the first 20 prime numbers: auto primes20 = primeNumbers<20>();

However, in that case, you cannot define the lambda inside a function scope. Generic lambdas allow you to define generic functionality locally inside a scope.

17.2Calling the Default Constructor of Lambdas

Lambdas provide a simple way to define function objects. If you define

auto cmp = [] (const auto& x, const auto& y) { return x > y;

};

this is equivalent to defining a class (the closure type) and creating an object of this class:

class NameChosenByCompiler { public:

template<typename T1, T2>

auto operator() (const T1& x, const T2& y) const { return x > y;

}

};

auto cmp = NameChosenByCompiler{};

The generated closure type has operator() defined, which means that you can use the lambda object cmp as a function:

cmp(val1, val); // yields the result of 42 > obj2

However, before C++20, the generated closure type had no callable default constructor and assignment operator. Objects of the generated class could only be initially created by the compiler. Only copying was possible:

auto cmp1 = [] (const auto& x, const auto& y) { return x > y;

};

 

auto cmp2 = cmp1;

// OK, copy constructor supported since C++11

decltype(cmp1) cmp3;

// ERROR until C++20: no default constructor provided

cmp1 = cmp2;

// ERROR until C++20: no assignment operator provided

17.2 Calling the Default Constructor of Lambdas

593

As a consequence, you could not easily pass a lambda as a sorting criterion or hash function to a container, where the type of the helper function was required. Consider a class Customer with the following interface:

class Customer

{

public:

...

std::string getName() const;

};

To use the name returned by getName() as the ordering criterion or value for the hash function, you had to pass both the type and the lambda as a template and call parameter:

// create balanced binary tree with user-defined ordering criterion:

auto lessName = [] (const Customer& c1, const Customer& c2) { return c1.getName() < c2.getName();

};

std::set<Customer, decltype(lessName)> coll1{lessName};

// create hash table with user-defined hash function: auto hashName = [] (const Customer& c) {

return std::hash<std::string>{}(c.getName());

};

std::unordered_set<Customer, decltype(hashName)> coll2{0, hashName};

The containers get the lambda when they are initialized so that they can use an internal copy of the lambda (for the unordered containers, you have to pass a minimum bucket size before). To compile, the type of the containers needs the type of the lambda.

Since C++20, lambdas with no captures have a default constructor and an assignment operator:

auto cmp1 = [] (const

auto& x, const auto& y) {

return x > y;

};

 

auto cmp2 = cmp1;

// OK, copy constructor supported

decltype(cmp1) cmp3;

// OK since C++20

cmp1 = cmp2;

// OK since C++20

For this reason, it is now enough to pass the type of the lambda for the ordering criterion or hash function respectively:

// create balanced binary tree with user-defined ordering criterion:

auto lessName = [] (const Customer& c1, const Customer& c2) {

return c1.getName() < c2.getName();

 

};

 

std::set<Customer, decltype(lessName)> coll1;

// OK since C++20

// create hash table with user-defined hash function:

594 Chapter 17: Lambda Extensions

auto hashName = [] (const Customer& c) {

return std::hash<std::string>{}(c.getName());

};

std::unordered_set<Customer, decltype(hashName)> coll2; // OK since C++20

This works because the argument for the ordering criterion or hash function has a default value, which is a default-constructed object of the type of the ordering criterion or hash function. And because lambdas without captures have a default constructor since C++20, the initialization of the ordering criterion with a default-constructed object of the type of the lambda now compiles.

You can even define the lambda inside the declaration of the container and use decltype to pass its type. For example, you can declare an associative container with an ordering criterion defined in the declaration as follows:

// create balanced binary tree with user-defined ordering criterion:

std::set<Customer,

 

decltype([] (const Customer& c1,

const Customer& c2) {

return c1.getName() <

c2.getName();

})> coll3;

// OK since C++20

In the same way, you can declare an unordered container with a hash function defined in the declaration as follows:

// create hash table with user-defined hash function: std::unordered_set<Customer,

decltype([] (const Customer& c) {

return std::hash<std::string>{}(c.getName());

})> coll;

// OK since C++20

See lang/lambdahash.cpp for a complete example.

 

17.3Lambdas as Non-Type Template Parameters

Since C++20, lambdas can be used as non-type template parameters (NTTPs): template<std::invocable auto GetVat>

int addTax(int value)

{

return static_cast<int>(std::round(value * (1 + GetVat())));

}

auto defaultTax = [] { // OK return 0.19;

};

std::cout << addTax<defaultTax>(100) << '\n';

This feature is a side effect of the new support for using literal types with public members only as nontype template parameter types. See the chapter about non-type template parameter extensions for a detailed discussion and complete example.

17.4 consteval Lambdas

595

17.4 consteval Lambdas

By using the new consteval keyword with lambdas, you can now require that lambdas become immediate functions so that “function calls” of them have to be evaluated at compile time. For example:

auto hashed = [] (const char* str) consteval {

...

};

auto hashWine = hashed("wine"); // hash() called at compile time

Due to the use of consteval in the definition of the lambda, any call has to happen at compile time with values known at compile time. Passing a runtime value is an error:

const char* s =

"beer";

 

auto hashBeer =

hashed(s);

// ERROR

constexpr const

char* cs = "water";

 

auto hashWater = hashed(cs);

// OK

Note that hashed itself does not have to be constexpr. It is a runtime object of the lambda for which the “function call” is performed at compile time.

The discussion of consteval for lambdas in the section about the new consteval keyword provides more details of this example.

You can also use the new template syntax for generic lambdas with consteval. This enables programmers to define the initialization of a compile-time function inside another function. For example:

// local compile-time computation of Num prime numbers: auto primeNumbers = [] <int Num> () consteval {

std::array<int, Num> primes; int idx = 0;

for (int val = 1; idx < Num; ++val) { if (isPrime(val)) {

primes[idx++] = val;

}

}

return primes;

};

See lang/lambdaconsteval.cpp for a complete program using this lambda.

Note that in this case, the template parameter is not deduced. Therefore, the syntax to explicitly specify the template parameter becomes a bit ugly:

auto primes = primeNumbers.operator()<100>();

Note also that you always have to provide the parameter list before consteval (the same applies when specifying constexpr there). You cannot skip the parentheses even if there is no parameter declared.