lambda表达式的作用

  1. 没有lambda表达式,STL中的“_if”族算法(比如std::find_if、std::remove_if和std::count_if等)恐怕只能使用最平凡的谓词来调用,这种情况同样发生在能够自定义比较函数的算法族(比如std::sort、std::nth_element和std::lower_bound等)。
  2. lambda表达式也能够用来为std::unique_ptr和std::shared_ptr快速创建自定义析构器,还能以同样直接的程度对谓词做特化处理来提供条件变量给线程API。
  3. 标准库之外,lambda表达式可以临时制作出回调函数、接口适配函数或是语境相关函数的特化版本以供一次性调用。
  4. 闭包是lambda式创建的运行期对象,根据不同的捕获模式,闭包会持有数据的副本或引用。闭包类就是实例化闭包的类。每个lambda表达式都会触发编译器生成一个独一无二的闭包类。而闭包中的语句会变成它的闭包类成员函数的可执行指令。
  5. lambda表达式常用于创建闭包并仅将其传递给函数的实参。一般而言,闭包可以复制,因此对于一个单独的闭包类别可以有多个闭包。

避免默认捕获模式

  1. C++11中有两种默认捕获模式,按引用或按值。按引用的默认捕获模式可能会导致空悬引用,按值的默认捕获模式貌似对空悬引用免疫,但是并没有,还会让我们认为自己的闭包是独立的(事实上它们可能不是独立的)。
  2. 按引用捕获会导致闭包包含指向局部变量的引用,或者指向定义lambda表达式内作用域形参的引用。一旦由lambda表达式所创建的闭包越过了该局部变量或形参的生命期,那么闭包内的引用就会空悬。
  3. 从长远观点来看,显式地列出lambda表达式所依赖的局部变量或形参是更好的软件工程实践,因为这样可以比较明显地看出是否有引用指向局部变量生命期过后导致空悬。
  4. 解决空悬引用的一个方法是采用按值的默认捕获模式,但是按值捕获了一个指针以后,在lambda闭包中持有的是这个指针的副本,但是我们无法阻止lambda以外的代码去针对该指针实施delete操作导致lambda表达室内的指针副本空悬。
  5. 捕获只能针对于在创建lambda式的作用域内可见的非静态局部变量(包括形参)。每一个非静态成员函数都持有一个this指针,然后每当提及该类的成员变量时都会用到该指针,因此成员函数内lambda闭包的存活与它含有其this指针副本的类对象的生命期是绑在一起的。这一特定问题可以通过将我们想捕获的成员变量复制到局部变量中,然后捕获该局部副本加以解决。
  6. 在C++14中,捕获成员变量的一种更好的方法是使用广义lambda捕获,对于广义lambda捕获而言没有按默认模式捕获一说。
  7. 使用默认值捕获模式的另外一个缺点是在于它似乎表明闭包是自洽的,与闭包外的数据变化绝缘,但是一般来说这是不正确的,因为lambda表达式不仅依赖于局部变量和形参(它们可以被捕获),它们还会依赖于静态存储期对象,这样的对象可以在lambda式内使用但是不能被捕获。此时如果使用了按默认值捕获模式,这些对象就给人错觉感觉是可以被捕获的。

使用初始化捕获将对象移入闭包

  1. 如果一个对象复制操作开销昂贵,移动操作开销低廉,C++11中未提供任何办法移动对象,但是C++14中为对象移入闭包提供了直接支持。
  2. C++14中可以使用初始化捕获实现按移动的捕获,使用初始化捕获,则你会得到机会指定: (1)由lambda生成的闭包类中的成员变量的名字(2)一个表达式,用以初始化该成员变量
     auto pw=std::make_unqiue<Widget>();
     auto func=[pw=std::move(pw)]{    //初始化捕获,使用std::move(pw)初始化闭包类中的成员
         return pw->isValidated()&&pw->isArchived();
     };
    
  3. 初始化捕获中位于“=”号左侧的是我们所制定的闭包类成员变量的名字,作用域就是闭包的作用域,右侧的是其初始化表达式,作用域则是与lambda表达式加以定义之处的作用域相同。初始化捕获的另一个名字是广义lambda捕获。
  4. 如果编译器缺少对C++14中初始化捕获的支持,由于lambda本质上是生成一个类,因此我们可以手动写一个类重载其移动构造函数以及调用运算符(),这样就完成了将对象移入这个类中并进行操作。
  5. 如果非想用lambda表达式,按移动捕获在C++11中可以采用以下方法模拟,只需要:
    (1)把需要捕获的对象移动到std::bind产生的函数对象中
    (2)给到lambda表达式一个指向欲“捕获”对象的引用
    和lambda表达式类似,std::bind也生成函数对象,bind的第一个实参是个可调用对象,接下来的所有实参表示传给该对象的值。std::bind返回的函数对象内部含有所有传递给std::bind的所有实参的副本,对于每个左值实参,在绑定对象内部对应的对象内对其实施的是复制构造,而对于每个右值实参,实施的则是移动构造。因此当实参为右值时,实施的是移动构造,该移动构造正是实现模拟移动捕获的核心所在,从而绕过C++11中无法将右值移入闭包的限制。
  6. 默认情况下,lambda生成的闭包类中的operator()成员函数会带有const饰词,因此闭包内的所有成员变量在lambda式的函数体内都会带有const饰词,但是绑定对象中移动构造得到的实参副本却不带有const饰词,因此为了防止该实参的副本在lambda式内被意外修改,lambda的形参就声明为常量引用。但如果lambda式的声明带有mutable饰词,则operator()函数就不会在声明时带有const饰词,此时就应该在lambda的声明中省略const。因此与C++14中移动捕获语义相同的模拟移动捕获的代码为:
     auto func=std::bind(
         [](const std::unqiue_ptr<Widget>& pw){
            return pw->isValidated()&&pw->isArchived(); 
         },std::make_unique<Widget>()
     );
    

对auto&&类型的形参使用decltype,以std::forward之

  1. C++14中引入泛型lambda表达式,lambda可以在形参类型中使用auto,这也代表闭包类中的operator()采用模板实现。
  2. 在泛型lambda表达式内部使用万能引用和完美转发时,我们可以通过探查形参的类型来确定传入的实参到底是左值还是右值,因此我们可以使用decltype,传入右值返回右值引用类型,传入左值返回左值引用类型。
  3. 使用std::forward的惯例是使用类型形参为左值引用表明想要返回左值,而用非引用类型时;来表明想要返回的是右值。当x为右值时,decltype(x)会产生右值引用类型,而非惯例的非引用,但是在引用折叠发生之后,传递给std::forward产生的结果与传递非引用的结果是相同的。
  4. C++14中的lambda表达式能够接受可变长形参,因此使用auto&&与std::forward就可以得到可以接受多个形参的完美转发lambda式版本。

优先使用lambda式,而非std::bind

  1. std::bind相比较于lambda表达式的可读性更差,占位符”_1”、”_2”代表在调用std::bind返回的函数对象时传递的第一个或第二个实参或作为”_1”、”_2”占位符所在位置的实参传递给std::bind中的第一个参数,也就是那个可调用对象。由于该实参的类型在实际调用过程中未加识别,所以还得查看可调用对象的声明应该传递何种类型的实参给std::bind的返回对象。
  2. 如果在std::bind中传递的绑定实参与时间等系统状态信息有关,此时是当std::bind的返回对象调用时就会生成,而不是其中的可调用对象调用时生成。因此要解决这一问题就要求知会std::bind以延迟表达式的评估求值到调用bind中可调用对象的时刻,要实现这一点就是在原来的std::bind中嵌套第二层std::bind的调用。
  3. 如果传递给std::bind的可调用对象有相应的重载,而对于std::bind的调用就无法通过编译,因为编译器无法确定应该将哪个版本的可调用对象传递给std::bind,因为std::bind拿到的信息就只有函数名,而函数名是多义的,为使std::bind通过编译需要将可调用对象强制类型转换到适当的函数指针类型。
  4. 由于可调用对象的调用是通过函数指针发生的,而编译器不太会内联掉通过函数指针发起的函数调用,那就表示使用std::bind返回函数对象的效率一般要比使用lambda的效率更低。
  5. std::bind总是复制其实参,但是调用方可以通过对某实参实施std::ref的手法达成按引用存储之的效果,此时std::bind内的可调用对象持有的是指向这个实参的引用,而不是其副本。而std::bind所返回的函数对象的所有实参都是按引用传递的,因为此种对象的函数调用运算符运用了完美转发。
  6. 在C++14中不存在std::bind的使用场景,而在C++11中仅在两个受限的场合还可以使用std::bind:
    (1)移动捕获,详见上一条款
    (2)多态函数对象,由于std::bind返回对象的函数调用运算符使用了完美转发,他就可以接受任何类型的实参(除了完美转发的限制情况)。这个特点在我们想要绑定的对象具有一个函数调用运算符模板时,是有利用价值的。比如:
     class PolyWidget{
         public:
         template<typename T>
         void operator()(const T& param);
     };
    

    但是在C++14中使用带有auto类型的形参就可以轻而易举地达成同样的效果。

  7. lambda表达式比起使用std::bind而言,可读性更好、表达能力更强,可能运行效率也会更高,因此尽量使用lambda表达式。