在 C++ 14 中使用初始化捕获生成 C++ Lambda 代码

C++ Lambda Code Generation with Init Captures in C++ 14

我正在尝试理解/阐明将捕获传递给 lambda 时生成的代码代码,尤其是在 C++14 中添加的广义初始化捕获中。

给出下面列出的代码示例这是我目前对编译器将生成什么的理解。

情况一:按值捕获/默认按值捕获

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

等于:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int x) : __x{x}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

所以有多个副本,一个复制到构造函数参数中,一个复制到成员中,这对于向量等类型来说会很昂贵

情况2:引用捕获/默认引用捕获

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

等于:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int& x) : x_{x}{}
    void operator()() const { std::cout << x << std::endl;}
private:
    int& x_;
};

参数是引用,成员是引用,所以没有副本。适合矢量等类型

案例 3:

通用初始化捕获

auto lambda = [x = 33]() { std::cout << x << std::endl; };

我的理解是这在某种意义上类似于案例1 它被复制到成员中。

我的猜测是编译器生成的代码类似于...

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name() : __x{33}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

此外,如果我有以下内容:

auto l = [p = std::move(unique_ptr_var)]() {
 // do something with unique_ptr_var
};

构造函数会是什么样子?是不是也搬进会员了?

案例 1 [x](){}:生成的构造函数将通过可能的 const 限定引用来接受其参数,以避免不必要的复制:

__some_compiler_generated_name(const int& x) : x_{x}{}

情况2 [x&](){}:你这里的假设是正确的,x通过引用传递和存储。


情况3 [x = 33](){}:再次正确,x按值初始化。


案例 4 [p = std::move(unique_ptr_var)]:构造函数将如下所示:

    __some_compiler_generated_name(std::unique_ptr<SomeType>&& x) :
        x_{std::move(x)}{}

所以是的,unique_ptr_var 是 "moved into" 闭包。另请参阅 Effective Modern C++ ("Use init capture to move objects into closures") 中 Scott Meyer 的第 32 条。

使用 cppinsights.io.

就不需要推测了

案例一:
代码

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}

编译器生成

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

案例二:
代码

#include <iostream>
#include <memory>

int main() {
    int x = 33;
    auto lambda = [&x]() { std::cout << x << std::endl; };
}

编译器生成

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int & x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int & _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

案例三:
代码

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}

编译器生成

#include <iostream>

int main()
{

  class __lambda_4_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_4_16(const __lambda_4_16 &) = default;
    // inline /*constexpr */ __lambda_4_16(__lambda_4_16 &&) noexcept = default;
    public: __lambda_4_16(int _x)
    : x{_x}
    {}

  };

  __lambda_4_16 lambda = __lambda_4_16(__lambda_4_16{33});
}

案例4(非正式):
代码

#include <iostream>
#include <memory>

int main() {
    auto x = std::make_unique<int>(33);
    auto lambda = [x = std::move(x)]() { std::cout << *x << std::endl; };
}

编译器生成

// EDITED output to minimize horizontal scrolling
#include <iostream>
#include <memory>

int main()
{
  std::unique_ptr<int, std::default_delete<int> > x = 
      std::unique_ptr<int, std::default_delete<int> >(std::make_unique<int>(33));

  class __lambda_6_16
  {
    std::unique_ptr<int, std::default_delete<int> > x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x.operator*()).operator<<(std::endl);
    }

    // inline __lambda_6_16(const __lambda_6_16 &) = delete;
    // inline __lambda_6_16(__lambda_6_16 &&) noexcept = default;
    public: __lambda_6_16(std::unique_ptr<int, std::default_delete<int> > _x)
    : x{_x}
    {}

  };

  __lambda_6_16 lambda = __lambda_6_16(__lambda_6_16{std::unique_ptr<int, 
                                                     std::default_delete<int> >
                                                         (std::move(x))});
}

我相信最后一段代码可以回答您的问题。移动发生,但不是 [技术上] 在构造函数中。

捕获本身不是 const,但您可以看到 operator() 函数是。当然,如果您需要修改捕获,则将 lambda 标记为 mutable.

这个问题无法用代码完全回答。您也许可以编写一些 "equivalent" 代码,但标准并未以这种方式指定。

说完这些,让我们深入探讨 [expr.prim.lambda]. The first thing to note is that constructors are only mentioned in [expr.prim.lambda.closure]/13:

The closure type associated with a lambda-expression has no default constructor if the lambda-expression has a lambda-capture and a defaulted default constructor otherwise. It has a defaulted copy constructor and a defaulted move constructor ([class.copy.ctor]). It has a deleted copy assignment operator if the lambda-expression has a lambda-capture and defaulted copy and move assignment operators otherwise ([class.copy.assign]). [ Note: These special member functions are implicitly defined as usual, and might therefore be defined as deleted. — end note ]

所以马上就应该清楚构造函数不是正式定义捕获对象的方式。您可以非常接近(参见 cppinsights.io 答案),但细节有所不同(请注意案例 4 的答案中的代码如何无法编译)。


这些是讨论案例 1 所需的主要标准条款:

[expr.prim.lambda.capture]/10

[...]
For each entity captured by copy, an unnamed non-static data member is declared in the closure type. The declaration order of these members is unspecified. The type of such a data member is the referenced type if the entity is a reference to an object, an lvalue reference to the referenced function type if the entity is a reference to a function, or the type of the corresponding captured entity otherwise. A member of an anonymous union shall not be captured by copy.

[expr.prim.lambda.capture]/11

Every id-expression within the compound-statement of a lambda-expression that is an odr-use of an entity captured by copy is transformed into an access to the corresponding unnamed data member of the closure type. [...]

[expr.prim.lambda.capture]/15

When the lambda-expression is evaluated, the entities that are captured by copy are used to direct-initialize each corresponding non-static data member of the resulting closure object, and the non-static data members corresponding to the init-captures are initialized as indicated by the corresponding initializer (which may be copy- or direct-initialization). [...]

让我们将其应用于您的案例 1:

Case 1: capture by value / default capture by value

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

这个 lambda 的闭包类型将有一个类型为 int 的未命名非静态数据成员(我们称之为 __x)(因为 x 既不是引用也不是函数),并且在 lambda 主体中对 x 的访问被转换为对 __x 的访问。当我们评估 lambda 表达式时(即当分配给 lambda 时),我们 direct-initialize __xx.

简而言之,只有一个副本发生。不涉及闭包类型的构造函数,在"normal" C++中无法表达这一点(注意闭包类型is not an aggregate type也是如此)。


引用捕获涉及 [expr.prim.lambda.capture]/12:

An entity is captured by reference if it is implicitly or explicitly captured but not captured by copy. It is unspecified whether additional unnamed non-static data members are declared in the closure type for entities captured by reference. [...]

还有一段关于引用的引用捕获,但我们没有在任何地方这样做。

因此,对于情况 2:

Case 2: capture by reference / default capture by reference

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

我们不知道是否有成员被添加到闭包类型中。 lambda 主体中的 x 可能直接引用外部的 x。这由编译器决定,它将以某种形式的中间语言(不同编译器不同)执行此操作,而不是 C++ 代码的源转换。


[expr.prim.lambda.capture]/6 中详细介绍了初始化捕获:

An init-capture behaves as if it declares and explicitly captures a variable of the form auto init-capture ; whose declarative region is the lambda-expression's compound-statement, except that:

  • (6.1) if the capture is by copy (see below), the non-static data member declared for the capture and the variable are treated as two different ways of referring to the same object, which has the lifetime of the non-static data member, and no additional copy and destruction is performed, and
  • (6.2) if the capture is by reference, the variable's lifetime ends when the closure object's lifetime ends.

鉴于此,让我们看一下案例 3:

Case 3: Generalised init capture

auto lambda = [x = 33]() { std::cout << x << std::endl; };

如前所述,将其想象成一个由 auto x = 33; 创建并由副本显式捕获的变量。此变量仅在 lambda 主体内 "visible"。如前文 [expr.prim.lambda.capture]/15 所述,闭包类型的相应成员(后代的 __x )的初始化是由给定的初始化程序在评估 lambda 表达式时进行的。

为了避免疑义:这并不意味着这里的东西被初始化了两次。 auto x = 33; 是一个 "as if" 来继承简单捕获的语义,所描述的初始化是对这些语义的修改。只发生一次初始化。

这也包括案例 4:

auto l = [p = std::move(unique_ptr_var)]() {
  // do something with unique_ptr_var
};

闭包类型成员在评估 lambda 表达式时由 __p = std::move(unique_ptr_var) 初始化(即当 l 被分配给时)。在 lambda 主体中对 p 的访问被转换为对 __p.

的访问

TL;DR:只执行了最少数量的 copies/initializations/moves(就像 hope/expect 一样)。 我假设 lambda 是 不是根据源转换(不同于其他语法糖)指定 因为根据构造函数表达事物将需要多余的操作。

我希望这能解决问题中表达的恐惧:)