在C ++ 14中具有Init Capture的C ++ Lambda代码生成

Bla*_*son 9 c++ lambda move c++14

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

提供下面列出的以下代码示例,这是我目前对编译器将生成的内容的理解。

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

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };
Run Code Online (Sandbox Code Playgroud)

等于:

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;
};
Run Code Online (Sandbox Code Playgroud)

因此,存在多个副本,一个副本要复制到构造函数参数中,而另一个副本要复制到成员中,这对于矢量等类型而言将是昂贵的。

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

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };
Run Code Online (Sandbox Code Playgroud)

等于:

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_;
};
Run Code Online (Sandbox Code Playgroud)

参数是引用,成员是引用,因此没有副本。非常适合矢量等类型。

情况3:

通用初始化捕获

auto lambda = [x = 33]() { std::cout << x << std::endl; };
Run Code Online (Sandbox Code Playgroud)

我的理解是,在将其复制到成员的意义上,这与案例1类似。

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

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

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

auto l = [p = std::move(unique_ptr_var)]() {
 // do something with unique_ptr_var
};
Run Code Online (Sandbox Code Playgroud)

构造函数是什么样的?它也将其移动到成员中吗?

lub*_*bgr 9

情况1 [x](){}:生成的构造函数将接受可能const带有限定符的引用的参数,以避免不必要的复制:

__some_compiler_generated_name(const int& x) : x_{x}{}
Run Code Online (Sandbox Code Playgroud)

情况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)}{}
Run Code Online (Sandbox Code Playgroud)

所以是的,unique_ptr_var“移入”了封闭。另请参见高效现代C ++中的Scott Meyer的第32项(“使用初始化捕获将对象移动到闭包中”)。


swe*_*ish 5

使用cppinsights.io无需进行推测。

情况1:
代码

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}
Run Code Online (Sandbox Code Playgroud)

编译器生成

#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});
}
Run Code Online (Sandbox Code Playgroud)

情况2:
代码

#include <iostream>
#include <memory>

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

编译器生成

#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});
}
Run Code Online (Sandbox Code Playgroud)

情况3:
代码

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}
Run Code Online (Sandbox Code Playgroud)

编译器生成

#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});
}
Run Code Online (Sandbox Code Playgroud)

案例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; };
}
Run Code Online (Sandbox Code Playgroud)

编译器生成

// 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))});
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 如果感兴趣,请链接到该问题:https://github.com/andreasfertig/cppinsights/issues/258 我仍然推荐该网站进行诸如测试 SFINAE 以及是否会发生隐式转换之类的事情。 (2认同)

Max*_*hof 2

这个问题无法用代码完全回答。您也许能够编写一些“等效”的代码,但标准并未以这种方式指定。

\n\n

抛开这些,让我们深入探讨一下[expr.prim.lambda]。首先要注意的是,构造函数仅在以下内容中提到[expr.prim.lambda.closure]/13

\n\n
\n

如果lambda 表达式具有lambda捕获,则与lambda 表达式关联的闭包类型没有默认构造函数,否则具有默认的默认构造函数。它有一个默认的复制构造函数和一个默认的移动构造函数([class.copy.ctor])。如果lambda 表达式具有 lambda 捕获,则它具有已删除的复制赋值运算符,否则具有默认的复制和移动赋值运算符 ([class.copy.assign])。[注意:这些特殊成员函数像平常一样隐式定义,因此可能被定义为已删除。\xe2\x80\x94尾注]

\n
\n\n

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

\n\n
\n\n

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

\n\n

[expr.prim.lambda.capture]/10

\n\n
\n

[...]
\n 对于复制捕获的每个实体,在闭包类型中声明一个未命名的非静态数据成员。\n 这些成员的声明顺序未指定。\n 这样的数据成员的类型是如果实体是对对象的引用,则为引用类型;如果实体是对函数的引用,则为所引用函数类型的左值引用;否则为相应捕获实体的类型。\n 匿名联合的成员不得被副本捕获。

\n
\n\n

[expr.prim.lambda.capture]/11

\n\n
\n

lambda 表达式的复合语句中的每个id 表达式(通过复制捕获的实体的 odr 使用)都会转换为对闭包类型的相应未命名数据成员的访问。[...]

\n
\n\n

[expr.prim.lambda.capture]/15

\n\n
\n

当计算 lambda 表达式时,通过复制捕获的实体用于直接初始化生成的闭包对象的每个对应的非静态数据成员,并且与 init-captures 对应的非静态数据成员被初始化为由相应的初始化器指示(可以是复制初始化或直接初始化)。[...]

\n
\n\n

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

\n\n
\n

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

\n\n
int x = 6;\nauto lambda = [x]() { std::cout << x << std::endl; };\n
Run Code Online (Sandbox Code Playgroud)\n
\n\n

__x此 lambda 的闭包类型将具有类型int(因为x既不是引用也不是函数)的未命名非静态数据成员(我们称之为),并且对xlambda 主体内的访问将转换为对 的访问__x。当我们计算 lambda 表达式时(即赋值给 时lambda),我们直接用 进行 __x初始化x

\n\n

简而言之,只发生一个副本。不涉及闭包类型的构造函数,并且不可能用“正常”C++ 来表达这一点(请注意,闭包类型也不是聚合类型)。

\n\n
\n\n

参考捕获涉及[expr.prim.lambda.capture]/12

\n\n
\n

如果实体被隐式或显式捕获但未通过复制捕获,则该实体通过引用捕获。未指定是否在通过引用捕获的实体的闭包类型中声明其他未命名的非静态数据成员。[...]

\n
\n\n

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

\n\n

因此,对于情况 2:

\n\n
\n

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

\n\n
int x = 6;\nauto lambda = [&x]() { std::cout << x << std::endl; };\n
Run Code Online (Sandbox Code Playgroud)\n
\n\n

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

\n\n
\n\n

初始化捕获的详细信息如下[expr.prim.lambda.capture]/6

\n\n
\n

auto init-capture ;init-capture 的行为就好像它声明并显式捕获其声明区域是 lambda 表达式的复合语句的形式的变量,但以下情况除外:

\n\n
    \n
  • (6.1)\n 如果捕获是通过复制进行的(见下文),则为捕获声明的非静态数据成员和变量将被视为引用同一对象的两种不同方式,该对象具有非静态数据成员的生命周期静态数据成员,并且不执行额外的复制和销毁,并且
  • \n
  • (6.2)\n 如果捕获是通过引用进行的,则变量的生命周期将在闭包对象的生命周期结束时结束。
  • \n
\n
\n\n

鉴于此,我们来看看案例 3:

\n\n
\n

案例 3:通用初始化捕获

\n\n
auto lambda = [x = 33]() { std::cout << x << std::endl; };\n
Run Code Online (Sandbox Code Playgroud)\n
\n\n

如前所述,将其想象为由auto x = 33;副本创建并显式捕获的变量。该变量仅在 lambda 体内“可见”。如前所述[expr.prim.lambda.capture]/15,闭包类型的相应成员(__x对于后代)的初始化是由给定的初始化程序在计算 lambda 表达式时进行的。

\n\n

为了避免疑问:这并不意味着这里的东西被初始化了两次。这auto x = 33;是继承简单捕获的语义的“好像”,并且所描述的初始化是对这些语义的修改。仅发生一次初始化。

\n\n

这也涵盖了情况 4:

\n\n
\n
auto l = [p = std::move(unique_ptr_var)]() {\n  // do something with unique_ptr_var\n};\n
Run Code Online (Sandbox Code Playgroud)\n
\n\n

闭包类型成员是__p = std::move(unique_ptr_var)在 lambda 表达式求值时(即l赋值时)初始化的。p对lambda 主体中的访问会转换为对 的访问__p

\n\n
\n\n

TL;DR:仅执行最少数量的副本/初始化/移动(正如人们所希望/期望的那样)。我假设 lambda 不是按照源转换(与其他语法糖不同)来指定的,因为用构造函数来表达事物需要多余的操作。

\n\n

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

\n