c++ 11引入的Lambdas和用c++ 14引入的generic Lambdas都是成功的例子。lambda的使用使得我们在一些地方比函数方便很多。
c++ 17改进了lambda的功能,允许在更多的地方使用lambdas:
-
在constant表达式(即,在编译时期);
-
在需要当前对象副本的地方(例如,在线程中调用lambdas时)。
1. constexpr lambda
从c++ 17起,如果lambda表达式符合编译时期constexpr的要求的话,lambdas会隐式转换为constexpr表达式。也就是说,任何lambda都可以在编译时上下文中使用,前提是它使用的特性对编译时上下文中有效。即满足如下要求:
- lambda表达式内没有静态变量;
- lambda表达式内没有虚函数;
- lambda表达式内没有 try/catch语句;
- lambda表达式内没有new/delete;
传递一个编译时期的值做为lambda表示的参数,并用lambda表达式求平方的结果用来声明std::array<>:
例1:
#include <iostream>
#include <array>
auto squared = [](auto val) { // implicitly constexpr since C++17
return val * val;
};
int main()
{
std::array<int, squared(5)> arr; // OK since C++17 => std::array<int,25>
auto size = arr.size();
return 0;
}
结果如下:
constexpr lambda也就意味着还可以在constexpr 函数中用lambda表达式了,这在C++17之前是不允许的。这样使用constexpr函数和普通函数没多大区别了,使用起来非常舒服。下面是constexpr lambda的例子:
#include <iostream>
template <typename I>
constexpr auto func(I i)
{
//use a lambda in constexpr context
return [i](auto j) { return i + j; };
}
int main(void)
{
const auto ret = func(10)(2);
std::cout << ret << std::endl;
return 0;
}
运行结果如下:
如果不满足constexpr lambda的要求,则不能转换为constexpr表达式,但是仍然可以在运行时期使用lambda:
auto squared2 = [](auto val) {
static int calls = 0; // OK, but disables lambda for constexpr contexts
...
return val*val;
};
std::array<int,squared2(5)> a; // ERROR: static variable in compile-time context
std::cout << squared2(5) << '\n'; // OK
如果不能确定lambda是否是constexpr表达式,可以将其声明为constexpr来让编译器判断:
auto squared3 = [](auto val) constexpr { // OK since C++17
return val*val;
};
对于指定返回类型的constexpr lambda语法如下:
auto squared3i = [](int val) constexpr -> int { // OK since C++17
return val*val;
};
对于lambda表达式,如果声明了constexpr但是又使用了不满足constexpr lambda的语句,则为编译错误:
例2:
#include <iostream>
#include <array>
auto squared4 = [](auto val) constexpr
{
static int calls = 0; // ERROR: static variable in compile-time context
return val * val;
};
int main()
{
constexpr int size = squared4(2);
return 0;
}
编译错误信息如下:
对于隐式或显式的constexpr lambda,函数调用操作符是constexpr。也就是说,如下定义:
auto squared = [](auto val) { // implicitly constexpr since C++17
return val*val;
};
转换为闭包类型:
class CompilerSpecificName
{
public:
...
template<typename T>
constexpr auto operator() (T val) const
{
return val*val;
}
};
注意,这里生成的闭包类型的函数调用操作符是自动constexpr。一般来说,自从c++ 17,如果lambda被显式定义为constexpr,或者它是隐式的constexpr(就像这里的情况一样),那么生成的函数调用操作符就是constexpr。
#include <iostream>
#include <array>
int main()
{
constexpr auto add1 = [](int n, int m){
auto func1 = [=] { return n; }; //func1 lambda表达式
auto func2 = [=] { return m; }; //func2 lambda表达式
return [=] { return func1() + func2(); }; //注意返回的是lambda表达式类型
};
constexpr auto add2 = [](int n, int m){
return n + m;
};
auto add3 = [](int n, int m){
return n + m;
};
int sum1 = add1(30, 40)( ); //传入常量值,add1在编译期计算,立即返回70,由于add1返回的是lambda表达式类型,所以第二个()是必须的;
int sum2 = add2(sum1, 4); //由于传入非constexpr变量,add2的constexpr失效,变成运行期lambda
constexpr int sum3 = add3(1, 2); //sum3为constexpr变量,传入常量值,add3变成编译期lambda,立即返回3
int sum4 = add2(10, 2);//传入常量值,add2在编译期计算,立即返回12
constexpr auto add4 = [](int n, int m){
auto func1 = [=] { return n; }; //func1 lambda表达式
auto func2 = [=] { return m; }; //func2 lambda表达式
return func1() + func2(); //注意返回的是整形,不是lambda表达式类型
};
{
constexpr int sum1 = add4(30, 40);//传入常量值,add1在编译期计算,立即返回70
int sum2 = add2(sum1, 4); //由于传入的参数都是constexpr变量,add2的constexpr生效,也是编译期的lambda,立即返回74
constexpr int sum3 = add3(1, 2); //sum3为constexpr变量,传入常量值,add3变成编译期lambda,立即返回3
int sum4 = add2(sum2, 2);//传入非constexpr常量值,add2的constexpr失效,变成运行期lambda
std::array<int, add4(30, 40)> arr;
std::array<int, sum1> arr2;
std::array<int, add2(sum1, 4)> arr3;
std::array<int, sum3> arr4;
}
return 0;
}
2. 传递this拷贝到lambda
当在成员函数中使用lambda时,您不能隐式地访问调用成员函数的对象。也就是说,在lambda内部,如果不以任何形式捕获它,就不能使用对象的成员(独立于是否用this->限定它们):
#include <iostream>
#include <string>
class NoThisCaptureInLambda
{
private:
std::string name;
public:
void foo()
{
auto l1 = [] { std::cout << name << '\n'; }; // ERROR
auto l2 = [] { std::cout << this->name << '\n'; }; // ERROR
}
};
int main()
{
return 0;
}
编译错误信息如下:
在c++ 11和c++ 14中,您必须通过值或引用传递此值:
#include <iostream>
#incldue <string>
class ThisCaptureInLambda
{
private:
std::string name;
public:
void foo()
{
auto l1 = [this] { std::cout << name << '\n'; }; // OK
auto l2 = [=] { std::cout << name << '\n'; }; // OK
auto l3 = [&] { std::cout << name << '\n'; }; // OK
}
};
然而这里的问题是,即使进行了this捕获,也是通过引用捕获了底层对象(因为只复制了this指针)。如果lambda的生存期超过调用成员函数的对象的生存期,这就会成为一个问题。一个关键的例子是当lambda定义一个新线程的任务时,该线程应该使用它自己的对象副本来避免任何并发性或生存期问题。另一个原因可能只是传递对象的副本及其当前状态。
从c++ 14开始就有了一种变通方法:
例3:
#include <iostream>
#include <string>
class ThisCopyInLambda
{
private:
std::string name;
public:
ThisCopyInLambda(std::string name) : name{ name }
{
}
void foo()
{
auto l1 = [*this](){
std::cout << name << '\n';
};
l1();
}
};
int main()
{
ThisCopyInLambda thisCopy("This copy test in lambda");
thisCopy.foo();
return 0;
}
结果如下:
虽然满足了我们的期望,但是它的可读性差。而且,程序员在使用=或&捕获其他对象时仍然可能意外地使用name这个变量:
例4:
#include <iostream>
#include <string>
class ThisCopyInLambda
{
private:
mutable std::string name;
public:
ThisCopyInLambda(std::string name): name{name}
{
}
void foo()
{
auto l1 = [=, thisCopy = *this](){
thisCopy.name = "new name";
std::cout << thisCopy.name << std::endl;
std::cout << name << '\n'; //still the old name
name = "this is temorary string";
std::cout << name << '\n'; //the new value
std::cout << thisCopy.name << std::endl; //和name是两个独立的对象
};
l1();
}
void display()
{
std::cout << name << '\n';
}
};
int main()
{
ThisCopyInLambda thisCopy("This copy test in lambda");
thisCopy.foo();
thisCopy.display();
return 0;
}
结果如下:
从c++ 17,你可以显式地要求捕获当前对象的副本,方法是捕获*this:
#include <iostream>
#include <string>
class ThisCopyInLambda
{
private:
mutable std::string name;
public:
ThisCopyInLambda(std::string name): name{name}
{
}
void foo()
{
auto l1 = [*this](){
std::cout << name << std::endl;
name = "this is temorary string";
std::cout << name << '\n';
};
l1();
}
void display()
{
std::cout << name << '\n';
}
};
int main()
{
ThisCopyInLambda thisCopy("This copy test in lambda");
thisCopy.foo();
thisCopy.display();//still old value
return 0;
}
也就是说,捕获*this意味着将当前对象的副本传递给lambda。你仍然可以将捕获*this与其他捕获相结合,只要和this没有矛盾:
auto l2 = [&, *this] { ... }; // OK
auto l3 = [this, *this] { ... }; // ERROR
一个完整的例子:
例3:
#include <iostream>
#include <string>
#include <thread>
class Data {
private:
std::string name;
public:
Data(const std::string& s) : name(s) {
}
std::thread startThreadWithCopyOfThis() const
{
// start and return new thread using this after 3 seconds:
std::thread t([*this]
{
std::cout << "I will shellp 3 seconds" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << name << std::endl;
});
return t;
}
};
int main()
{
std::thread t;
{
Data d{ "This copy capture in C++17" };
t = d.startThreadWithCopyOfThis();
} // d已经销毁
std::cout << "the main thread wait for sub thread end." << std::endl;
t.join();
return 0;
}
结果如下:
如果修改13代码中的*this为this,则结果可能如下:
lambda中的*this是一个拷贝,这意味着传递了d的一个拷贝。因此,线程在调用d的析构函数后使用传递的对象是没有问题的。
如果我们用[this]、[=]或[&]捕获了,那么线程将运行未定义的行为,因为在传递给线程的lambda中打印name时,lambda将使用已销毁对象的成员。
C++17中,我们可以在lambda表达式的捕获类别里[]写上*this,表示传递到lambda中的是this对象的拷贝。从而解决上述的问题。(注:C++11中是不允许这样写的。成员捕获列表中只能是变量、”=“、”&“、”=, 变量列表“、”&, 变量列表“ )