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.
引用&参考:
- Effective Modern C++ 条款31 对于lambda表达式,避免使用默认捕获模式
- Effective Modern C++ 条款32 对于lambda,使用初始化捕获来把对象移动到闭包
- Effective Modern C++ 条款33 对需要std::forward的auto&&参数使用decltype
- Effective Modern C++ 条款34 比起std::bind更偏向使用lambda
- (cppreference)std::forward