第17章 Lambda扩展
# 第17章 Lambda扩展
本章介绍C++20为lambda表达式引入的补充特性。
# 17.1 带模板参数的泛型Lambda
C++20引入了一项扩展,允许在泛型lambda中使用模板参数。你可以在捕获子句和调用参数(如果有的话)之间指定这些模板参数:
auto foo = []<typename T>(const T& param) { // 自C++20起可行
T tmp{}; // 使用模板参数的类型声明对象
...
};
2
3
4
lambda的模板参数在声明泛型参数时,为类型或类型的一部分提供了命名的便利。例如:
[]<typename T>(T* ptr) { // 自C++20起可行
... // 可以将T用作ptr指向的值的类型
};
2
3
或者:
[]<typename T, int N>(T (&arr)[N]) {
... // 可以将T用作传递数组的元素类型,N用作数组大小
};
2
3
如果你想知道为什么在这些情况下不使用函数模板,要记住lambda具有一些函数无法提供的优势:
- 它们可以在函数内部定义。
- 它们可以捕获运行时的值,以便在运行时指定其功能行为。
- 你可以将它们作为参数传递,而无需指定参数类型。
# 17.1.1 在实践中为泛型Lambda使用模板参数
显式模板参数对于特化(或部分限制参数类型)泛型lambda很有用。考虑以下示例:
[]<typename T>(const std::vector<T>& vec) { // 只能传递向量
...
};
2
3
这个lambda只接受向量作为参数。如果使用auto
,很难将参数限制为向量,因为C++(目前)还不支持类似std::vector<auto>
这样的写法。不过,在这种情况下,你也可以使用类型约束来限制参数的类型(例如要求随机访问,甚至是特定类型)。
显式模板参数还有助于避免使用decltype
。例如,在lambda中对泛型参数包进行完美转发时,你可以这样写:
[]<typename... Types>(Types&&... args) {
foo(std::forward<Types>(args)...);
};
2
3
而不是:
[] (auto&&... args) {
foo(std::forward<decltype(args)>(args)...);
};
2
3
类似的例子是在访问std::variant<>
(C++17中引入)时,为特定类型提供特殊行为的代码:
std::variant<int, std::string> var;
...
// 调用具有类型特定行为的泛型lambda:
std::visit([](const auto& val) {
if constexpr(std::is_same_v<decltype(val), const std::string&>) {
... // 字符串特定的处理
}
std::cout << val << "\n";
},
var);
2
3
4
5
6
7
8
9
10
我们必须使用decltype()
来获取参数的类型,并将该类型作为const&
进行比较(或者去掉const
和引用)。自C++20起,你可以这样写:
std::visit([]<typename T>(const T& val) { // 自C++20起
if constexpr(std::is_same_v<T, std::string>) {
... // 字符串特定的处理
}
std::cout << "value : " << val << "\n";
},
var);
2
3
4
5
6
7
你还可以为consteval
lambda声明模板参数,以强制其在编译时执行。
# 17.1.2 显式指定Lambda模板参数
lambda为定义函数对象(仿函数)提供了一种便捷方式。对于泛型lambda,其函数调用运算符(operator()
)是一个模板。通过使用指定参数名而非auto
的语法,在生成的函数调用运算符中,你就有了模板参数的名称。
例如,如果你定义以下lambda:
auto primeNumbers = [] <int Num> () {
std::array<int, Num> primes{};
... // 计算并赋值前Num个质数
return primes;
};
2
3
4
5
编译器会定义一个相应的闭包类型:
class NameChosenByCompiler {
public:
...
template<int Num>
auto operator() () const {
std::array<int, Num> primes{};
... // 计算并赋值前Num个质数
return primes;
}
};
2
3
4
5
6
7
8
9
10
并创建该类的一个对象(如果没有捕获值,则使用默认构造函数):
auto primeNumbers = NameChosenByCompiler{};
要显式指定模板参数,在将lambda作为函数使用时,必须将其传递给operator()
:
// 用前20个质数初始化数组:
auto primes20 = primeNumbers.operator()<20>();
2
除了使用间接调用,在指定模板参数时,无法避免指定operator()
。
你可以尝试通过将模板参数设为编译时常量来使其可推导,但结果语法并不更优1:
auto primeNumbers = [] <int Num> (std::integral_constant<int, Num>) {
std::array<int, Num> primes{};
... // 计算并赋值前Num个质数
return primes;
};
// 用前20个质数初始化数组:
auto primes20 = primeNumbers(std::integral_constant<int,20>{});
2
3
4
5
6
7
1感谢Arthur O’Dwyer和Jonathan Wakely指出这一点。
或者,你可能会想到使用变量模板,这是C++14中引入的技术。通过这种方式,你可以使变量primeNumbers
成为泛型的,而不是使lambda成为泛型的:
template<int Num>
auto primeNumbers = [] () {
std::array<int, Num> primes{};
... // 计算并赋值前Num个质数
return primes;
};
...
// 用前20个质数初始化数组:
auto primes20 = primeNumbers<20>();
2
3
4
5
6
7
8
9
然而,在这种情况下,你不能在函数作用域内定义lambda。泛型lambda允许你在作用域内局部定义泛型功能。
# 17.2 调用Lambda的默认构造函数
Lambda表达式提供了一种定义函数对象的简便方法。如果你定义:
auto cmp = [] (const auto& x, const auto& y) {
return x > y;
};
2
3
这等同于定义一个类(闭包类型)并创建该类的一个对象:
class NameChosenByCompiler {
public:
template<typename T1, T2>
auto operator() (const T1& x, const T2& y) const {
return x > y;
}
};
auto cmp = NameChosenByCompiler{};
2
3
4
5
6
7
8
生成的闭包类型定义了operator()
,这意味着你可以将lambda对象cmp
当作函数使用:
cmp(val1, val); // 得到42 > obj2的结果
然而,在C++20之前,生成的闭包类型没有可调用的默认构造函数和赋值运算符。生成类的对象只能由编译器初始创建。仅支持复制:
auto cmp1 = [] (const auto& x, const auto& y) {
return x > y;
};
auto cmp2 = cmp1; // 没问题,自C++11起支持复制构造函数
decltype(cmp1) cmp3; // 直到C++20都报错:未提供默认构造函数
cmp1 = cmp2; // 直到C++20都报错:未提供赋值运算符
2
3
4
5
6
7
因此,当容器需要辅助函数的类型时,你无法轻松地将lambda作为排序准则或哈希函数传递给容器。考虑一个具有以下接口的Customer
类:
class Customer
{
public:
...
std::string getName() const ;
};
2
3
4
5
6
要将getName()
返回的名称用作排序准则或哈希函数的值,你必须同时将类型和lambda作为模板参数和调用参数传递:
// 使用用户定义的排序准则创建平衡二叉树:
auto lessName = [] (const Customer& c1, const Customer& c2) {
return c1.getName() < c2.getName();
};
std::set<Customer, decltype(lessName)> coll1{lessName};
// 使用用户定义的哈希函数创建哈希表:
auto hashName = [] (const Customer& c) {
return std::hash<std::string>{}(c.getName());
};
std::unordered_set<Customer, decltype(hashName)> coll2{0, hashName};
2
3
4
5
6
7
8
9
10
11
容器在初始化时获取lambda,以便它们可以使用lambda的内部副本(对于无序容器,你必须先传递最小桶大小)。为了使代码能够编译,容器的类型需要lambda的类型。
自C++20起,无捕获的lambda具有默认构造函数和赋值运算符:
auto cmp1 = [] (const auto& x, const auto& y) {
return x > y;
};
auto cmp2 = cmp1; // 没问题,支持复制构造函数
decltype(cmp1) cmp3; // 自C++20起没问题
cmp1 = cmp2; // 自C++20起没问题
2
3
4
5
6
7
因此,现在分别传递排序准则或哈希函数的lambda类型就足够了:
// 使用用户定义的排序准则创建平衡二叉树:
auto lessName = [] (const Customer& c1, const Customer& c2) {
return c1.getName() < c2.getName();
};
std::set<Customer, decltype(lessName)> coll1; // 自C++20起没问题
// 使用用户定义的哈希函数创建哈希表:
auto hashName = [] (const Customer& c) {
return std::hash<std::string>{}(c.getName());
};
std::unordered_set<Customer, decltype(hashName)> coll2; // 自C++20起没问题
2
3
4
5
6
7
8
9
10
11
这之所以可行,是因为排序准则或哈希函数的参数有一个默认值,该默认值是排序准则或哈希函数类型的默认构造对象。并且由于自C++20起无捕获的lambda具有默认构造函数,现在用lambda类型的默认构造对象初始化排序准则可以正常编译。
你甚至可以在容器声明内部定义lambda,并使用decltype
传递其类型。例如,你可以声明一个在声明中定义了排序准则的关联容器,如下所示:
// 使用用户定义的排序准则创建平衡二叉树:
std::set<Customer,
decltype([] (const Customer& c1, const Customer& c2) {
return c1.getName() < c2.getName();
}
)> coll3; // 自C++20起没问题
2
3
4
5
6
同样,你可以声明一个在声明中定义了哈希函数的无序容器,如下所示:
// 使用用户定义的哈希函数创建哈希表:
std::unordered_set<Customer,
decltype([] (const Customer& c) {
return std::hash<std::string>{}(c.getName());
})> coll; // 自C++20起没问题
2
3
4
5
完整示例见lang/lambdahash.cpp
。
# 17.3 Lambda作为非类型模板参数
自C++20起,lambda可以用作非类型模板参数(NTTPs):
template<std::invocable auto GetVat>
int addTax(int value)
{
return static_cast<int>(std::round(value * (1 + GetVat())));
}
auto defaultTax = [] { // 没问题
return 0.19;
};
std::cout << addTax<defaultTax>(100) << "\n";
2
3
4
5
6
7
8
9
10
此功能是新支持将仅具有公共成员的字面量类型用作非类型模板参数类型的一个附带结果。有关详细讨论和完整示例,请参阅关于非类型模板参数扩展的章节。
# 17.4 consteval
修饰的lambda表达式
通过在lambda表达式中使用新的consteval
关键字,你现在可以要求lambda成为立即函数,这样对它们的 “函数调用” 就必须在编译时进行求值。例如:
auto hashed = [] (const char* str) consteval {
// ...
};
auto hashWine = hashed("wine "); // hash()在编译时调用
2
3
4
由于在lambda定义中使用了consteval
,任何调用都必须在编译时使用编译时已知的值进行。传递运行时值会报错:
const char* s = "beer ";
auto hashBeer = hashed(s); // 错误
constexpr const char* cs = "water ";
auto hashWater = hashed(cs); // 正确
2
3
4
注意,hashed
本身不必是constexpr
。它是一个lambda的运行时对象,对其的 “函数调用” 在编译时执行。
在关于新consteval
关键字的章节中对consteval
修饰的lambda表达式的讨论,提供了这个示例的更多细节。
你也可以将新的模板语法用于带consteval
的泛型lambda表达式。这使得程序员能够在另一个函数内部定义编译时函数的初始化。例如:
// 本地编译时计算Num个素数:
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;
};
2
3
4
5
6
7
8
9
10
11
使用这个lambda表达式的完整程序见lang/lambdaconsteval.cpp
。
注意,在这种情况下,模板参数不会被推导。因此,显式指定模板参数的语法会有点难看:
auto primes = primeNumbers.operator()<100>();
还要注意,你必须始终在consteval
之前提供参数列表(在那里指定constexpr
时也是如此)。即使没有声明参数,也不能省略括号。
# 17.5 捕获规则的变化
C++20对lambda表达式中值和对象的捕获引入了几个新的扩展。
# 17.5.1 捕获this
和*this
如果在成员函数内部定义lambda表达式,问题在于如何访问调用该成员函数的对象的数据。在C++20之前,我们有以下规则:
class MyType {
std::string name;
// ...
void foo() {
int val = 0;
// ...
auto l0 = [val] { bar(val, name); }; // 错误:未捕获成员name
auto l1 = [val, name=name] { bar(val, name); }; // 正确,按值捕获val和name
auto l2 = [&] { bar(val, name); }; // 正确(按引用捕获val和name)
auto l3 = [&, this] { bar(val, name); }; // 正确(按引用捕获val和name)
auto l4 = [&, *this] { bar(val, name); }; // 正确(按引用捕获val,按值捕获name)
auto l5 = [=] { bar(val, name); }; // 正确(按值捕获val,按引用捕获name)
auto l6 = [=, this] { bar(val, name); }; // C++20之前错误
auto l7 = [=, *this] { bar(val, name); }; // 正确(按值捕获val和name)
// ...
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
自C++20起,适用以下规则:
class MyType {
std::string name;
// ...
void foo() {
int val = 0;
// ...
auto l0 = [val] { bar(val, name); }; // 错误:未捕获成员name
auto l1 = [val, name=name] { bar(val, name); }; // 正确,按值捕获val和name
auto l2 = [&] { bar(val, name); }; // 已弃用(按引用捕获val和name)
auto l3 = [&, this] { bar(val, name); }; // 正确(按引用捕获val和name)
auto l4 = [&, *this] { bar(val, name); }; // 正确(按引用捕获val,按值捕获name)
auto l5 = [=] { bar(val, name); }; // 已弃用(按值捕获val,按引用捕获name)
auto l6 = [=, this] { bar(val, name); }; // 正确(按值捕获val,按引用捕获name)
auto l7 = [=, *this] { bar(val, name); }; // 正确(按值捕获val和name)
// ...
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
因此,自C++20起,有以下变化:
[=, this]
现在允许作为lambda捕获(一些编译器之前就允许,尽管在形式上它是无效的)。- 隐式捕获
*this
已弃用。
# 17.5.2 捕获结构化绑定
自C++20起,允许捕获结构化绑定(C++17中引入):
std::map<int, std::string> mymap;
// ...
for (const auto& [key,val] : mymap) {
auto l = [key, val] { // 自C++20起正确
// ...
};
// ...
}
2
3
4
5
6
7
8
一些编译器之前就允许捕获结构化绑定,尽管在形式上它是无效的。
# 17.5.3 捕获可变参数模板的参数包
如果你有一个可变参数模板,可以如下捕获参数包:
template<typename... Args>
void foo(Args... args)
{
auto l1 = [&] {
bar(args...); // 正确
};
// ...
}
2
3
4
5
6
7
8
然而,如果你想返回创建的lambda表达式以供后续使用,就会有问题:
- 使用
[&]
,你会返回一个引用已销毁参数包的lambda表达式。 - 使用
[args...]
或[=]
,你会复制传递的参数包。
通常,在捕获对象时可以使用初始化捕获来使用移动语义:
template<typename T>
void foo(T arg)
{
auto l3 = [arg = std::move(arg)] { // 自C++14起正确
bar(arg); // 正确
};
// ...
}
2
3
4
5
6
7
8
然而,之前没有提供用于参数包的初始化捕获的语法。C++20引入了相应的语法:
template<typename... Args>
void foo(Args... args)
{
auto l4 = [...args = std::move(args)] { // 自C++20起正确
bar(args...); // 正确
};
// ...
}
2
3
4
5
6
7
8
你也可以通过引用初始化捕获参数包。例如,我们可以为lambda表达式更改参数名称,如下所示:
template<typename... Args>
void foo(Args... args)
{
auto l4 = [&...fooArgs = args] { // 自C++20起正确
bar(fooArgs...); // 正确
};
// ...
}
2
3
4
5
6
7
8
# 捕获可变参数模板参数包的示例
一个创建并返回一个按值捕获可变数量参数的lambda的泛型函数,现在的写法如下:
template<typename Callable, typename... Args>
auto createToCall(Callable op, Args... args) {
return [op, ...args = std::move(args)] () -> decltype(auto) {
return op(args...);
};
}
2
3
4
5
6
使用缩写函数模板的新语法,代码如下:
auto createToCall(auto op, auto... args) {
return [op, ...args = std::move(args)] () -> decltype(auto) {
return op(args...);
};
}
2
3
4
5
下面是一个完整的示例:
// lang/capturepack.cpp
#include <iostream>
#include <string_view>
auto createToCall(auto op, auto... args) {
return [op, ...args = std::move(args)] () -> decltype(auto) {
return op(args...);
};
}
void printWithGAndNoG(std::string_view s) {
std::cout << s << "g " << s << "\n";
}
int main() {
auto printHero = createToCall(printWithGAndNoG, "Zhan ");
...
printHero();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
运行它来打印一个英雄的名字!
# 17.5.4 作为协程的lambda
lambda也可以是协程,协程是在C++20中引入的。不过要注意,在这种情况下,lambda不应捕获任何内容,因为协程的使用时长很可能超过局部创建的lambda对象的存在时长。
# 17.6 补充说明
通用lambda的模板语法最初由路易斯·迪翁(Louis Dionne)在http://wg21.link/p0428r0 (opens new window)中提出。最终被接受的措辞由路易斯·迪翁在http://wg21.link/p0428r2 (opens new window)中制定。通用lambda的模板语法也曾由路易斯·迪翁在http://wg21.link/p0624r0 (opens new window)中提出,最终被接受的措辞由他在http://wg21.link/p0624r2 (opens new window)中确定。允许lambda作为非类型模板参数这一特性,由杰夫·斯奈德(Jeff Snyder)和路易斯·迪翁在http://wg21.link/p0732r2 (opens new window)中最终确定。
对consteval
lambda的支持由理查德·史密斯(Richard Smith)、安德鲁·萨顿(Andrew Sutton)和达韦德·范德沃德(Daveed Vandevoorde)在http://wg21.link/p1073r3 (opens new window)中最终确定。
捕获this
和*this
规则的变更最初由托马斯·科普(Thomas Köppe)在http://wg21.link/p0409r0 (opens new window)和http://wg21.link/p0806r0 (opens new window)中提出。最终被接受的措辞由托马斯·科普在http://wg21.link/p0409r2 (opens new window)和http://wg21.link/p0806r2 (opens new window)中制定。
捕获结构化绑定由尼古拉·莱塞尔(Nicolas Lesser)在http://wg21.link/p1091r3 (opens new window)中最终确定。
初始化捕获参数包由巴里·列夫津(Barry Revzin)在http://wg21.link/p0780r2 (opens new window)和http://wg21.link/p2095r0 (opens new window)中确定并被接受。