(Effective Modern C++) – 第六章

Item 31: Avoid default capture modes.

默认捕获包括引用捕获和值捕获([&]和[=])。尽量避免直接使用默认的捕获模式:

  • 当使用引用捕获的时候,捕获的对象的可能被销毁(lambda表达式和对象的生命期不一样)。显式地指定捕获地对象同样会有问题。

void addDivisorFilter()
{
  auto calc1 = computeSomeValue1();
  auto calc2 = computeSomeValue2();
  auto divisor = computeDivisor(calc1, calc2);
  filters.emplace_back(                              // danger!
    [&](int value) { return value % divisor == 0; }  // ref to
  );                                                 // divisor
}
  • 使用值捕获的时候,捕获类内的对象可能包含对象的指针。
class A {
public:
	A(int i):divisor(i){}
	void add() const;
private:
	int divisor;
};
void A::add() const
{
	int cpd = this->divisor;
	//funcs.emplace_back([=](int d)->int {return d / divisor; });//comment1:这样还是捕获了this。this所指的对象还是被销毁了
	funcs.emplace_back([=](int d)->int {return d / cpd; });//comment2:使用拷贝
}

comment1部分:捕获divisor的时候同时捕获了对象(*this)。所以当使用lambda表达式的时候,对象有可能已经析构从而this空悬divisor的值就不能确定了

lambda只能默认捕获只限定于可见的非静态的自动变量(lambda作用域可见。例如形参等)而没指定捕获模式只能捕获静态的变量。对于捕获divisor而言:如果使用默认的捕获([=]和[&])就会捕获this指针;如果没有指定捕获的模式([])则会无法编译出现错误(VC++):错误 C4573 “A::divisor”的用法要求编译器捕获“this”,但当前默认捕获模式不允许使用“this” 

解决办法:comment2部分使用了拷贝,默认捕获模式可以捕获局部变量(值捕获)。

  • 默认值捕获捕获了静态变量。所以使用值捕获同样与其字面上的意义不同:
void addDivisorFilter()
{
    static auto calc1 = computeSomeValue1(); // static
    static auto calc2 = computeSomeValue2(); // static

    static auto divisor = computeDivisor(calc1, calc2);   // static

    filters.emplace_back(
      [=](int value)                    // 没有捕获任何东西
      { return value % divisor == 0; }  // 引用了上面的divisor
    );

    ++divisor;          // 修改divisor
};

每次调用addDivisorFilter函数,divisor都会递增。而每个lambda并没有捕获divisor而是引用了divisor,所以每次调用这些lambda的divisor的值取决于调用addDivisorFilter函数的次数。

void s(){
	static int dd1 = 10;
	funcs.emplace_back([=](int d)->int {return d / dd1; });
	dd1 = dd1 + 10;
}
int main()
{
	s();
	s();
	int u=funcs[0](100);
	int u2 = funcs[1](100);
	system("pause");
	return 0;
}

u和u2都是3。

Things to Remember
• Default by-reference capture can lead to dangling references.
• Default by-value capture is susceptible to dangling pointers (especially this),
and it misleadingly suggests that lambdas are self-contained.

Item 32: Use init capture to move objects into closures.

C++11的lambda表达式不支持移动对象到闭包。C++14可以使用init capture(初始化捕获,或者generalized lambda capture)来:

  • 从表达式中初始话闭包内的变量名
  • 命名成员变量的名字
class widget
{
public:
	bool isBool() {
		return true;
	}
	bool isInt() {
		return false;
	}
	widget(int g, double hg) {}
	~widget() { std::cout << "dtr" << std::endl; }
	widget() { std::cout << "ctr" << std::endl; }
private:
};
...
auto pw = std::make_unique<Widget>(); 
auto func = [pw = std::move(pw)]               // init data mbr
            { return pw->isValidated()         // in closure w/
                     && pw->isArchived(); };   // std::move(pw)

pw = std::move(pw)的意思是:在闭包中创建一个成员变量pw,然后用——对局部变量pw使用std::move的——结果初始化那个成员变量。通常,lambda体内代码的作用域在闭包类内,所以代码中的pw指的是闭包类的成员变量。

如果std::make_unique创建的对象的状态(例如英语初始化成员变量pw)适合被lambda捕获,则局部变量pw可以不需要。直接使用pw = std::make_unique<widget>()然后调用unique的move构造函数初始化成员变量pw变量即可。

C++11模拟move:

  • 需要把捕获的对象移动std::bind产生的函数中
  • 给lambda一个要“捕获”对象的引用(作为参数)。
std::vector<double> data;        // 如前

...           // 添加数据

auto func =               // 引用捕获的C++11模仿物
    std::bind(
      [](const std::vector<double>& data)     // 代码关键部分!
      { /* uses of data */ },
      std::move(data);              // 代码关键部分!
   );

类似于lambda表达式,std::bind产生一个函数对象。我把std::bind返回的函数对象称为bind object(绑定对象)。std::bind的第一个参数是一个可执行对象,后面的参数代表传给可执行对象的值。默认情况下bind使用值拷贝(std::ref可以进行引用传递)。当绑定对象被调用的时候,存储的参数(bind执行时已经被求值)将传递给可执行对象。

默认情况下,lambda生成的类的operator()函数是const的(也就是不能更改捕获的对象),如果需要修改则使用mutable修饰:

auto func = 
    std::bind(                             // 可变lambda,初始化捕获的C++11模仿物
      [](std::vector<double>& data) mutable
      { /* uses of data */ },
      std::move(data);
  );
int d=10;
	auto d = [=]() { d = 5;//error:d不可以修改。需要mutable修饰
	};

data于lambda的生命期相同,只要闭包存在,则模拟的移动的对象(使用std::move返回的对象的拷贝)就存在(他们的生命期相同)。

C++14的直接初始化:

auto func = [pw = std::make_unique<Widget>()]   // 如前,在闭包内创建pw
            { return pw->isValidated() && pw->isArchived(); };

C++11模拟:

auto func = std::bind(
              [](const std::unique_ptr<Widget>& pw)
              { return pw->isValidated() && pw->isArchived(); },
              std::make_unique<Widget>()
           );
Things to Remember
• Use C++14’s init capture to move objects into closures.
• In C++11, emulate init capture via hand-written classes or std::bind.

Item 33: Use decltype on auto&& parameters to std::forward them.

lambda表达式也可以是=实现类似于完美转发的功能。

#include <utility>
#include <memory>
#include <typeinfo>
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
class widget
{
public:
	bool isBool() {
		return true;
	}
	bool isInt() {
		return false;
	}
	~widget() { std::cout << "dtr" << std::endl; }
	widget() { std::cout << "ctr" << std::endl; }
private:
};

int main()
{
	std::vector<int> con;//如果放在main函数以外。自动成为static类型。而static类型不需要捕获
	int f;
	auto ff = [=](auto x) mutable {con.push_back(x);};
	ff(25);
	ff(3.6);
	system("pause");
	return 0;
}

C++14的forward的实现:

template<typename T>     // 在命名空间std
T&& forward(remove_reference_t<T>& param)
{
    return static_cast<T&&>(param);
}

decltype(x):

  • x为右值的时候返回X&&;x为左值,返回X&;x为非引用类型,返回类型X

forward<X>(x):

  • x为右值转发的类型X&&;x为左值转发类型为X&;x为非引用类型转发为X&&

所以decltype(x)传递给std::forward是可以得到想要的结果的

Things to Remember
• Use decltype on auto&& parameters to std::forward them.

Item 34: Prefer lambdas to std::bind.

std::bind的一些缺点:

  • 参数默认是值传递的。如果需要引用传递。使用std::ref即可
  • placeholder、绑定对象的参数实在std::bind被调用的时候求值而不是绑定对象所绑定的函数被调用的时候(这对于一些类似定时器的程序可能出现精度的问题)。所以需要延迟求值:
using namespace std::chrono;
using namespace std::literals;

using namespace std::placeholders;   // "_1"需要这个命名空间

auto setSoundB =                  // "B"对应"bind"
    std::bind(setAlarm,
              steady_clock::now() + 1h,    // 错误!看下面
              _1,
              30s);

时间+1h是在bind调用时求值而不是setAlarm函数调用的时候!延迟求值(将参数求职时间与调用函数的时间同步)

auto setSoundB = 
    std::bind(setAlarm,
              std::bind(std::plus<>(), steady_clock::now(), 1h),
              _1,
              30s);

C++14中,标准类型的模板参数可以省略。std::plus<type>,type由1h+steady_clock::now()推导出。

  • std::bind函数绑定的函数名不能自动重载。而需要将指定的重载版本转换为函数指针:
enum class Volume { Normal, Loud, LoudPlusPlus };

void setAlarm(Time t, Sound s, Duration d, Volume v);
void setAlarm(Time t, Sound s, Duration d);

lambda自动重载:

auto setSoundL =                  // 和以前
    [](Sound s)
    {
        using namespace std::chrono;
        using namespace std::literals;

        setAlarm(steady_clock::now + 1h,    // 正确,调用
                 s,                         // 3参数版本的
                 30s);                      // setAlarm
    };

std::bind需要转换为重载版本的指针

using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);

auto setSoundB =   // 现在就ok了
    std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
              std::bind(std::plus<>(),
                        steady_clocl::now()
                        1h),
              _1,
              30s);
  • std::bind通过函数指针调用函数,而lambda可能以通用的方式内联

std::bind在C++11的应用:

  • “实现”类似C++14的auto类型推导
class PolyWidget {
public:
    template<typename T>
    void operator() (const T& param);
    ...
};
...
PolyWidget pw;

auto boundPW = std::bind(pw, _1);
...
boundPW(1930);      // 传递int到PolyWidget::operator()

boundPW(nullptr);   // 传递nullptr到PolyWidget::operator()

boundPW("Rosebud");   // 传递字符串到PolyWidget::operator()
Things to Remember
• Lambdas are more readable, more expressive, and may be more efficient than
using std::bind.
• In C++11 only, std::bind may be useful for implementing move capture or
for binding objects with templatized function call operators.

引用&参考: