【问题标题】:C++ Lambda Code Generation with Init Captures in C++ 14在 C++ 14 中使用初始化捕获生成 C++ Lambda 代码
【发布时间】:2019-10-08 12:29:25
【问题描述】:

我试图理解/澄清在将捕获传递给 lambda 时生成的代码代码,尤其是在 C++14 中添加的通用初始化捕获中。

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

案例1:按值捕获/默认按值捕获

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
};

构造函数会是什么样子?它是否也将其移动到成员中?

【问题讨论】:

  • @rafix07 在这种情况下,生成的洞察代码甚至无法编译(它尝试复制初始化参数中的唯一 ptr 成员)。 cppinsights 对于获取一般要点很有用,但它显然无法在这里回答这个问题。
  • 您似乎假设将 lambda 转换为函子作为编译的第一步,或者您只是在寻找等效的代码(即相同的行为)?特定编译器生成代码的方式(以及它生成的代码)将取决于编译器、版本、体系结构、标志等。那么,您是否要求特定平台?如果不是,那么您的问题无法真正回答。除了实际生成的代码可能会比您列出的仿函数更有效(例如,内联构造函数、避免不必要的副本等)。
  • 如果您对 C++ 标准对此的规定感兴趣,请参阅 [expr.prim.lambda]。在这里总结为一个答案太多了。

标签: c++ lambda c++14 move


【解决方案1】:

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

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

案例 2 [x&amp;](){}:您这里的假设是正确的,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 被“移入”了闭包。另请参阅 Scott Meyer 在 Effective Modern C++ 中的第 32 条(“使用初始化捕获将对象移动到闭包中”)。

【讨论】:

  • "const-qualified" 为什么?
  • @cpplearner Mh,好问题。我想我插入了那个是因为其中一种心理自动症被踢了^^ 至少const 在这里不会受到伤害,因为非const 等时存在一些歧义/更好的匹配等。无论如何,你认为我应该删除@ 987654334@?
  • 我认为 const 应该保留,如果传递给的参数实际上是 const 呢?
  • 所以你是说这里发生了两个移动(或复制)结构?
  • 对不起,我的意思是情况 4(用于移动)和情况 1(用于副本)。根据您的陈述,我的问题的副本部分毫无意义(但我质疑这些陈述)。
【解决方案2】:

使用cppinsights.io,无需猜测。

案例 1:
代码

#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});
}

案例 2:
代码

#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});
}

案例 3:
代码

#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

【讨论】:

  • 您为最后一种情况显示的代码甚至无法编译。该代码无法支持“发生了移动,但在构造函数中没有[技术上]”的结论。
  • 案例 4 的 代码 肯定可以在我的 Mac 上编译。我很惊讶 从 cppinsights 生成的扩展代码 无法编译。到目前为止,该网站对我来说非常可靠。我会向他们提出一个问题。编辑:我确实确认生成的代码无法编译;没有这个编辑就不清楚了。
  • 如果感兴趣,请链接到该问题:github.com/andreasfertig/cppinsights/issues/258 我仍然推荐该站点用于测试 SFINAE 以及是否会发生隐式转换。
【解决方案3】:

这个问题无法用代码完全回答。您也许可以编写一些“等效”的代码,但标准并没有这样指定。

除此之外,让我们深入了解[expr.prim.lambda]。首先要注意的是[expr.prim.lambda.closure]/13中只提到了构造函数:

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

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


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

[expr.prim.lambda.capture]/10

[...]
对于通过副本捕获的每个实体,在闭包类型中声明了一个未命名的非静态数据成员。 这些成员的声明顺序是未指定的。 如果实体是对对象的引用,则此类数据成员的类型为被引用类型;如果实体是对函数的引用,则为对被引用函数类型的左值引用,否则为相应捕获实体的类型。 不得抄袭匿名工会成员。

[expr.prim.lambda.capture]/11

lambda-expression 的复合语句中的每个 id-expression 是对通过复制捕获的实体的 odr-use 转换为对闭包类型的相应未命名数据成员。 [...]

[expr.prim.lambda.capture]/15

当计算 lambda-expression 时,通过 copy 捕获的实体用于直接初始化生成的闭包对象的每个对应的非静态数据成员,以及与 init-captures 对应的非静态数据成员由相应的初始化程序(可以是复制初始化或直接初始化)指示进行初始化。 [...]

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

案例1:按值捕获/默认按值捕获

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

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

简而言之,只会复制一份。不涉及闭包类型的构造函数,也无法在“普通”C++ 中表达这一点(注意闭包类型is not an aggregate type 也一样)。


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

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

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

所以,对于案例 2:

案例2:引用捕获/默认引用捕获

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

我们不知道成员是否被添加到闭包类型中。 lambda 主体中的x 可能只是直接引用外部的x。这取决于编译器,它会以某种形式的中间语言(编译器与编译器不同)来实现这一点,而不是 C++ 代码的源代码转换。


初始化捕获在[expr.prim.lambda.capture]/6中有详细说明:

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

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

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

案例 3:广义初始化捕获

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

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

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

这也包括案例 4:

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

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


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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-01-30
    • 2019-03-25
    • 1970-01-01
    • 1970-01-01
    • 2014-02-18
    • 1970-01-01
    • 2021-01-07
    相关资源
    最近更新 更多