C++17之lambda扩展

30 篇文章 17 订阅

 

 

c++ 11引入的Lambdas和用c++ 14引入的generic Lambdas都是成功的例子。lambda的使用使得我们在一些地方比函数方便很多。
c++ 17改进了lambda的功能,允许在更多的地方使用lambdas:

  1. 在constant表达式(即,在编译时期);

  2. 在需要当前对象副本的地方(例如,在线程中调用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中是不允许这样写的。成员捕获列表中只能是变量、”=“、”&“、”=, 变量列表“、”&, 变量列表“ )
 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值