译者注:本文翻译自:https://www.fluentcpp.com/2021/12/13/the-evolutions-of-lambdas-in-c14-c17-and-c20/
序
lambda表达式是现代C++中最流行功能之一,自从C++11被引进以来,在C++的代码中变得无处不在。同时lambda表达式也行进化获得重要的功能,这些功能能够帮助写出更强表现力的代码。进而因为lambda变得如此的通用,花一些时间学习如何使用它也是有价值的。
这里我们的目标是涵盖演进过程中的主要的点,没能包含所有的细节。对lambda表达式的全面的讲述更适合在一本书而不是一篇文章。如果你想了解更多的话,这里推荐Bartek的书 - C++ Lambda Story,里边会讲述所有的细节。
lambda的进化就是赋予它手动定义函数对象的能力
本篇文章假设你已经了解C++11中的基础知识,所以我们从C++14开始讨论
C++14的lambda
C++14中,lambda表达式主要4点主要的改进:
- 默认参数
- 模板参数
- 广义捕获
- 从函数中返回lambda
默认参数
在C++14中,lambda表达式可以使用默认参数,和函数类似:
auto myLambda = [](int x, int y = 0) {
std::cout << x << '-' << y << '\n';
};
std::cout << myLambda(1, 2) << '\n';
std::cout << myLambda(1) << '\n';
输出为:
1-2
1-0
模板参数
C++11中我们必须定义lambda的参数类型:
auto myLambda = [](int x) { std::cout << x << '\n'; };
C++14中我们可以通过这些书写使得接收所有的类型参数:
auto myLambda = [](auto&& x) { std::cout << x << '\n'; };
即使你不需要处理多种类型的情况下,这中书写方式也十分有用,可以避免重复,且代码更加紧凑和可读,举例来说:
auto myLambda = [](namespace1::namespace2::namespace3::ACertainTypeOfWidget const& widget) {
std::cout << widget.value() << '\n';
};
这个可以使用以下写法来优化:
auto myLambda = [](auto&& widget) {
std::cout << widget.value() << '\n';
};
译者注:书写更加简单
广义的捕获
C++11中lambda只能捕获作用域内存在的对象:
int z = 42;
auto myLambda = [z](int x){
std::cout << x << '-' << z + 2 << '\n';
};
但是随着在C++14中有了强有力的广义的捕获能力,我们可以使用任何变量来初始化捕获值,以下是简单例子:
int z = 42;
auto myLambda = [y = z + 2](int x){
std::cout << x << '-' << y << '\n';
};
myLambda(1);
输出为:
1-44
从函数中返回lambda
受益于C++14的语言特性:从函数返回auto,而不用指定返回值类型。因为lambda表达式的类型是编译器生成的,所以在C++11中我们不能从函数中返回一个lambda表达式:
/* what type should we write here ?? */ f()
{
return [](int x){ return x * 2; };
}
译者注:因为lambda表达式的类型是编译器生成,C++11中不知道类型是什么,所以无法指定
在C++14中可以使用auto作为返回值类型,所以可能返回一个lambda表达式。这在一个表达式位于一大段代码中间的情况下很有用:
void f()
{
// ...
int z = 42;
auto myLambda = [z](int x)
{
// ...
// ...
// ...
};
// ...
}
我们可以包装lambda表达式在另外一个函数中,从而引入另一个抽象级别:
auto getMyLambda(int z)
{
return [z](int x)
{
// ...
// ...
// ...
};
}
void f()
{
// ...
int z = 42;
auto myLambda = getMyLambda(z);
// ...
}
如果想要了解更多这个知识点,请探索这个吸引人的话题:非正规的(out-of-line)lambda[https://www.fluentcpp.com/2020/06/05/out-of-line-lambdas/]
C++17的lambda
constexpr lambda
C++17的lambda有一个主要的优化:可以使用constexpr声明
constexpr auto times2 = [] (int n) { return n * 2; };
这样的lambda可以在编译期求值的上下文中使用:
static_assert(times2(3) == 6);
在模板编程中十分有用。
需要注意的是constexpr lambda在C++20中非常有用。事实上在C++20中std::vector和很多的STL算法都变成constexpr,与constexpr lambda一起在编译期能够完成复杂的计算。
有一个例外的容器std::array,在C++14中它的非变异(non-mutating)的访问就是constexpr,C++17中变异(mutating)访问操作也变成了constexpr了。
译者注:非变异操作指不会改变容器的内容的算法操作。变异操作刚好相反。
捕获*this的拷贝
C++17中另外一个lambda表达式的优化是用于捕获*this拷贝的简单语法,如下例子:
struct MyType{
int m_value;
auto getLambda()
{
return [this](){ return m_value; };
}
};
这个lambda捕获了this指针的拷贝,如果lambda表达式的寿命比这个this对象长的话,就会有内存问题。结合如下代码:
auto lambda = MyType{42}.getLambda();
lambda();
第一条语句结束后MyType就被析构了,第二句调用lambda时会访问this的成员变量m_value,但是这个this指针被析构了,这就会导致被定义的行为了,典型的就是程序奔溃。
解决这个问题的可能方案是拷贝整个对象。C++17提供以下语法来完成(注意this前边有*):
struct MyType
{
int m_value;
auto getLambda()
{
return [*this](){ return m_value; };
}
};
需要注意的是这个在C++14中已经可以通过广义捕获来实现相同的结果:
struct MyType
{
int m_value;
auto getLambda()
{
return [self = *this](){ return self.m_value; };
}
};
C++17这里只是让语法更简单易用
C++20的lambda
发展到C++20,lambda的功能就不及C++14,C++17基础了。C++20的lambda表达式的一个优化是定义模板的经典语法,使得更急接近手动定义函数对象。
auto myLambda = []<typename T>(T&& value){
std::cout << value << '\n';
};
这使得访问模板参数类型比C++14使用诸如auto&&这样的表达式模板lambda更容易
译者注:声明了模板参数比auto&&这样更加灵活,比如说函数参数的类型是要对T进行萃取操作后的类型(eg.std::remove_cv), 除此之外使用模板定义还可以使用concepts。
C++20的lambda另外一个优化是可以捕获一组可变参数:
template<typename... Ts>
void f(Ts&&... args)
{
auto myLambda = [...args = std::forward<Ts>(args)](){};
}
深入lambda
我们已经讨论了我认为是lambda从C++14到C++20的主要优化,但远不止此。这些主要特性附带了一些小东西,使得lambda代码更容易编写。
深入研究lambda对于更好地理解C++语言是一个很好的机会,我认为这是值得的时间投资的,更进一步说,我所知道的最好的资源是Bartek的C++ Lambda Story书籍,强烈推荐。
个人微信公众号
0号程序员