在 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 所需的主要标准条款:
[...]
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.
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. [...]
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 __x
和 x
.
简而言之,只有一个副本发生。不涉及闭包类型的构造函数,在"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 是 不是根据源转换(不同于其他语法糖)指定 因为根据构造函数表达事物将需要多余的操作。
我希望这能解决问题中表达的恐惧:)
我正在尝试理解/阐明将捕获传递给 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 所需的主要标准条款:
[...]
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.
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. [...]
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 __x
和 x
.
简而言之,只有一个副本发生。不涉及闭包类型的构造函数,在"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 是 不是根据源转换(不同于其他语法糖)指定 因为根据构造函数表达事物将需要多余的操作。
我希望这能解决问题中表达的恐惧:)