第六章 Lambda扩展

C++11引入了lambda,C++14引入了泛型lambda,这是一个成功的故事。lambda允许我们将功能指定为参数,这让定制函数的行为变得更加容易。

C++ 17进一步改进,允许lambda用在更多的地方。

6.1 constexpr lambda

自C++17后,只要可能,lambda就隐式地用constexpr修饰。也就是说,任何lambda都可以用于编译时上下文,前提是它使用的特性对编译时上下文有效(例如,仅字符串字面值,无静态变量,无virutal变量,无try/catch,无new/delete)。

举个例子,你可以传一个值给lambda,然后用计算的结果作为编译时的std::array<>大小:

auto squared = [](auto val) { // implicitly constexpr since C++17
  return val*val;
};
std::array<int,squared(5)> a; // OK since C++17 => std::array<int,25>

如果在不允许constexpr的上下文使用这个特性就不行,但是你仍然可以在运行时傻姑娘上下文使用lambda:

he lambda in run-time contexts:
auto squared2 = [](auto val) {      // implicitly constexpr since C++17
  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:

auto squared3 = [](auto val) constexpr {    // OK since C++17
  return val*val;
};

还可以指定返回类型,语法如下:

auto squared3i = [](int val) constexpr -> int { // OK since C++17
  return val*val;
};

constexpr对于函数的一般规则仍然有效:如果lambda在运行时上下文中使用,相应的功能在运行时执行。

然而,在不允许编译时上下文的地方使用constexpr lambda会得到一个编译时错误:

auto squared4 = [](auto val) constexpr {
  static int calls=0; // ERROR: static variable in compile-time context
  ...
  return val*val;
};

如果lambda式显式或隐式的constexpr,那么函数调用操作符也会是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。

6.2 传递this的拷贝到lambda

当在成员函数中使用lambda时,你不能隐式的访问调用这个成员函数的对象的成员。也就是说,在lambda内部,如果不捕获this,那么你不能使用这个对象的成员:

class C {
private:
    std::string name;
public:
    ...
    void foo() {
        auto l1 = [] { std::cout << name << '\n'; }; // ERROR
        auto l2 = [] { std::cout << this->name << '\n'; }; // ERROR
        ...
    }
};

C++11和C++14中可以传this引用或者传this值:

class C {
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的值,其底层捕获的仍然是引自对象(即只有指针被拷贝)。如果lambda的生命周期超过了对象的生命周期,这就会出现问题。一个重要的例子是当用lambda为新线程定义task,它应该使用对象的拷贝来避免任何并发或者生命周期问题。另一个原因可能只是传递一个对象的副本当前状态。

C++14有一个临时的解决方案,但是它读起来不好,工作起来也不好:

class C {
private:
    std::string name;
public:
    ...
    void foo() {
        auto l1 = [thisCopy=*this] { std::cout << thisCopy.name << '\n'; };
        ...
    }
};

举个例子,就算使用=&捕获了对象,开发者仍然可能不小心用到this

auto l1 = [&, thisCopy=*this] {
            thisCopy.name = "new name";
            std::cout << name << '\n'; // OOPS: still the old name
};

C++17开始,你可以显式地通过*this说明你想捕获当前对象的复制:

class C {
private:
    std::string name;
public:
    ...
    void foo() {
        auto l1 = [*this] { std::cout << name << '\n'; };
        ...
    }
};

捕获*this意味着当前对象的复制传递到了lambda。

在捕获了*this的情况下你仍然可以捕获其他this,只要没有与其他的发生冲突:

auto l2 = [&, *this] { ... };     // OK
auto l3 = [this, *this] { ... };  // ERROR

这里一个完整的例子:

// lang/lambdathis.cpp
#include <iostream>
#include <string>
#include <thread>

class Data {
private:
    std::string name;
public:
    Data(const std::string& s) : name(s) {
    }
    auto startThreadWithCopyOfThis() const {
        // start and return new thread using this after 3 seconds:
        using namespace std::literals;
        std::thread t([*this] {
            std::this_thread::sleep_for(3s);
            std::cout << name << '\n';
        });
        return t;
    }
};

int main()
{
    std::thread t;
    {
        Data d{"c1"};
        t = d.startThreadWithCopyOfThis();
    } // d is no longer valid
    t.join();
}

lambda用*this获取对象拷贝,即d。因此,即便是d的析构函数被调用后线程再使用传递的对象也没有问题。

如果我们使用[this],[=][&]捕获this,线程会产生未定义行为,因为在lambda打印name时,lambda使用的是已经析构后的对象的成员。

6.3 捕获引用

通过使用新的utility库函数,你现在可以捕获const对象引用

6.4 后记

constexpr最初由 Faisal Vali, Ville Voutilainen和Gabriel Dos Reis在https://wg21.link/n4487中提出。最后这个特性的公认措辞是由Faisal Vali, Jens Maurer和Richard Smith在https://wg21.link/p0170r1中给出。

捕获*this最初由H. Carter Edwards, Christian Trott, Hal Finkel, Jim Reus, Robin Maffeo和Ben Sander在https://wg21.link/p0018r0中提出。最后这个特性的公认措辞是由 H. Carter Edwards, Daveed Vandevoorde, Christian Trott, Hal Finkel, Jim Reus, Robin Maffeo和Ben Sander在https://wg21.link/p0180r3中给出。