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


9

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

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

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

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


1
@ rafix07在这种情况下,生成的洞察力代码甚至不会编译(它会尝试从参数中复制初始化唯一的ptr成员)。cppinsights对于获取一般要点很有用,但显然无法在此处回答此问题。
马克斯·朗霍夫

您似乎以为编译的第一步是将lambda转换为函子,还是只是在寻找等效的代码(即,相同的行为)?特定的编译器生成代码的方式(以及生成的代码)将取决于编译器,版本,体系结构,标志等。那么,您是否要求特定的平台?如果没有,您的问题就无法真正回答。除了实际生成的代码之外,其他代码可能会比您列出的函子更有效率(例如,内联构造函数,避免不必要的副本等)。
桑德·戴克

2
如果您对C ++标准必须说的内容感兴趣,请参考[expr.prim.lambda]。这里总结太多的答案。
桑德·戴克

Answers:


2

这个问题不能用代码完全回答。您也许可以编写一些“等效”的代码,但是没有以这种方式指定标准。

不用担心,让我们深入了解[expr.prim.lambda]。首先要注意的是,仅在[expr.prim.lambda.closure]/13以下内容中提到了构造函数:

与相关联的闭合类型λ-表达没有默认的构造,如果λ-表达具有λ-捕获和否则一个默认的默认构造函数。它具有默认的副本构造函数和默认的move构造函数([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 表达式的复合语句中的每个id 表达式(它是对通过副本捕获的实体的odr用法)都被转换为对闭包类型的相应未命名数据成员的访问。[...]

[expr.prim.lambda.capture]/15

评估lambda表达式时,将使用通过复制捕获的实体直接初始化生成的闭包对象的每个对应的非静态数据成员,并将与init-capture对应的非静态数据成员初始化为由相应的初始化程序(可以是复制初始化或直接初始化)指示。[...]

让我们将其应用于您的情况1:

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

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

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

简而言之,仅复制一份。不涉及闭包类型的构造函数,并且不可能用“普通” C ++来表示(请注意,闭包类型也不是聚合类型)。


参考记录涉及[expr.prim.lambda.capture]/12

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

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

因此,对于情况2:

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

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

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


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

初始化捕获的行为就像声明并显式捕获形式auto init-capture ;的变量一样,该变量的声明性区域是lambda表达式的复合语句,除了:

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

鉴于此,让我们看一下情况3:

情况3:通用init捕获

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

闭包类型成员是通过__p = std::move(unique_ptr_var)何时对lambda表达式求值(即何时l赋给)来初始化的。访问p在lambda体被变换成访问__p


TL; DR:仅执行最少数量的复制/初始化/移动(正如人们希望/期望的那样)。我会假设未按源转换的形式指定lambda (与其他语法糖不同)的原因恰恰是因为以构造函数的形式表示事物将需要多余的操作。

我希望这可以解决问题中表达的担忧:)


9

情况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“移入”了封闭。另请参见高效现代C ++中的Scott Meyer的第32项(“使用初始化捕获将对象移动到闭包中”)。


const合格”为什么?
cpplearner

@cpplearner Mh,好问题。我想我插入的原因是,其中一种精神自动主义开始const发挥作用^^至少由于非歧义性/更好的匹配性,在这里至少不会造成伤害const。无论如何,您认为我应该删除const吗?
lubgr

我认为const应该保留,如果传递给的参数实际上是const呢?
阿空加瓜

那么您是说这里发生了两个移动(或复制)构造?
Max Langhof

抱歉,我是指情况4(移动)和情况1(副本)。根据您的陈述,我的问题的副本部分毫无意义(但我对这些陈述表示怀疑)。
Max Langhof

5

使用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


您为最后一种情况显示的代码甚至不会编译。该代码不能支持“发生了移动,但没有(技术上)在构造函数中”的结论。
Max Langhof

情况4 的代码肯定可以在我的Mac上编译。令我惊讶的是,从cppinsights生成的扩展代码没有编译。到目前为止,该网站对我来说非常可靠。我会和他们提一个问题。编辑:我确实确认生成的代码不会编译;没有此修改,尚不清楚。
sweenish

1
如有兴趣,请链接到该问题:github.com/andreasfertig/cppinsights/issues/258 我仍然推荐该站点进行诸如测试SFINAE以及是否进行隐式强制转换的事情。
sweenish
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.