Effective Modern C++ 读书笔记之第六章

在 Thu 06 October 2016 发布于 读书笔记 分类

lambda表达式

Item 33 避免使用默认的变量捕获方式

上面说到c++11lambda表达式有两种变量的捕获方式:按值(复制)捕获和引用捕获。Item33在采用引用模式(默认[&]会捕获定义lambda表达式作用域下的所有变量, [&params]为显示捕获定义lambda表达式作用域下特定的变量)下,如果不加注意的话,lambda表达式很容易走出引用变量的作用域范围,造成悬空引用,会产生未定义的错误。

#include <iostream>
#include <functional>
#include <string>
class A
{
 public:
    A() : name_("default"){}
    void Print() const {
        std::cout << "A::Print: " << name_ << std::endl;
    }
 private:
        std::string name_;
};

typedef std::function<void()> func;

int main(int argc, char** argv)
{
    func target;
    {
        A a;
        auto f = [&](){ a.Print();};
        //auto f = [&a](){ a.Print();};
        target = f;
    }
    target();
    return 0;
}

在我自己机器下的测试结果为

A::Print:

但是如果是按值捕获的话,是没有问题的。但是这并不意味着按值捕获可以高枕无忧,如果捕获的是指针,如果在某一时刻该指针指向的内容被释放,在调用lambda表达式的时候,一样会产生悬空指针,使结果是未定义的行为。

在此Item33上,还说了一个特殊的情况,就是在类的成员函数里面定义的lambda表达式,并捕获类成员变量,实际上捕获的是类的this指针,如果lambda表达式在捕获成员变量后,走出类实例的作用域范围,一样造成悬空指针,产生未定义的行为,解决的方法是可以复制该成员函数然后按值捕获到lambda表达式中去。

Item 34 使用初始化捕获列表把参数移动到闭包中

这个Item 34解决的问题lambda怎样捕获那些没有copy语义只有move语义的变量,以及通过move语义来提供程序性能。c++11lambda表达式的捕获列表现在不支持move语义且不支持捕获列表的初始化,但是在c++14lambda表达式是支持捕获列表的初始化以及move语义,如:

#include <iostream>
#include <functional>
#include <string>
class A
{
 public:
    A() : name_("default"){}
    void Print() const {
        std::cout << "A::Print: " << name_ << std::endl;
    }

    A(const A& a) {
        name_ = a.name_;
    }

    A(A&& a) {
        name_ = std::move(a.name_);
        std::cout << "call A(A&& a)" << std::endl;
    }

 private:
        std::string name_;
};


int main(int argc, char** argv)
{
    A a;
    auto f = [a = std::move(a)](){ a.Print();};
    //auto f = [&a](){ a.Print();};
    f();
    a.Print();
    return 0;
}

使用c++14编译选项,测试结果为:

call A(A&& a)
A::Print: default
A::Print:

但是使用c++11编译选项,书上说目前c++11不支持,但是我这边的测试环境依然可以编译通过,并和使用c++14编译选项编译运行的结果一致。

lambda表达式的捕获参数的move语义可以借助std::bind来完成:

auto func = std:bind(
    [](A& a) {
        a.Print();
    },
    std::move(a)
);

对于std::bind来说,传递到其的参数,如果是左值,就会使用copy construct语义进行传递,如果是右值,就会通过move construct传递参数。在上面的例子中,data通过move construct语义传递到std::bind中,当func被调用的时候,data就会以引用的方式作为参数传入的lambda表达式中去,效果达到了lambda初始化捕获列表中的move语义。

Item 35

Item 36 lambda表达式优先于std::bind

书中从好几个方面认为lambdastd::bind更有优势(我更认为是编程经验,下面不做比较,纯做经验总结):

以书中例子为例:

using Time = std::chrono::time_point<std::chrono::steady_clock>;
enum class Sound {Beep, Siren, Whistle };
using Duration = std::chrono::steady_clock::duration;
void setAlarm(Time t, Sound s, Duration d);

使用lambda表达式:

//lambda
auot setSoundL =
  [](Sound s)
  {
    using namespace std::chrono;
    setAlarm(steady_clock::now() + hours(1),
             s,
             seconds(30));
  };

使用std::bind

using namespace std::chrono;
using namespace std::placeholders;
auto setSoundB =
  std::bind(setAlarm,
            steady_clock::now() + hours(1),
            _1,
            seconds(30));

在参数传递上,lambda显示的指明了传递参数的类型,而std::bind只提供了一个占位符来表示在调用setSoundB时要传入参数(),并且,如果需要传入的参数不止一个的话,即需要多个占位符,还需要不得不考虑占位符和传入的参数的顺序,在直观性上lambda有这很明显的优势。如果对setAlarm函数进行重载的话:

enum class Volume { Normal, Loud, LoudPlusPlus };
void setAlarm(Time t, Sound s, Duration d, Volume v);

上面的lambda表达式依然是可以使用的,但是std::bind就不行了,因为在绑定setAlarm函数时产生了二义性,不知道具体绑定那个函数,就需要手动编码明确指定具体要绑定那个函数:

using setAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB =
  std::bind(static_cast<setAlarm3ParamType>(setAlarm),
            steady_clock::now() + hours(1),
            _1,
            seconds(30));

并且,书中还说道lambda能会内联setAlarm函数,因此,lambda可能会比std::bind运行的稍微快些。

其次,在绑定参数(lambda为捕获参数)上, 正如上面说到的,std::bind在参数传入的时候,如果是左值就按照copy construct语义传递参数,如果是右值的话就是move construct语义传递参数,如果需要以引用方式传递参数的话,需要使用std::ref来实现。在lambda捕获参数的方式就不多复述了。