在C++20中使用`std::bit_cast`创建闭包(lambda)对象是否有效?
一位同事向我展示了一个 C++20 程序,其中使用std::bit_cast它捕获的值虚拟创建了一个闭包对象:
#include <bit>
#include <iostream>
class A {
int v;
public:
A(int u) : v(u) {}
auto getter() const {
if ( v > 0 ) throw 0;
return [this](){ return v; };
}
};
int main() {
A x(42);
auto xgetter = std::bit_cast<decltype(x.getter())>(&x);
std::cout << xgetter();
}
由于异常,此处main函数无法调用x.getter()。相反,它调用std::bit_cast将闭包类型作为模板参数,decltype(x.getter())并将&x为新闭包对象捕获的指针作为普通参数xgetter。然后xgetter被调用以获取 object 的值,x否则在main.
该程序被所有编译器接受,没有任何警告和打印42,演示:https : //gcc.godbolt.org/z/a479689Wa
但是程序是否按照标准格式良好,lambda 对象的这种“构造”是否有效?
回答
但是程序是否按照标准格式良好...
该程序有未定义的行为,条件是给实现者留有余地。特别取决于 lambda 的闭包类型
[this](){ return v; };
可以轻松复制;根据[expr.prim.lambda.closure]/2:
闭包类型在包含相应 lambda 表达式的最小块作用域、类作用域或命名空间作用域中声明。[...] 闭包类型不是聚合类型。一个实现可以定义与下面描述的不同的闭包类型,前提是这不会改变程序的可观察行为,除了改变:
- (2.1)闭合类型的大小和/或对齐方式,
- (2.2)闭包类型是否可简单复制([class.prop]),或
- (2.3) 闭包类型是否为标准布局类([class.prop])。[...]
这意味着是否满足[bit.cast]/1的约束:
template<class To, class From> constexpr To bit_cast(const From& from) noexcept;约束:
- (1.1)
sizeof(To) == sizeof(From)是true;- (1.2)
is_trivially_copyable_v<To>是true; 和- (1.3)
is_trivially_copyable_v<From>是true。
是实现定义的。
... lambda 对象的这种“构造”是否有效?
由于 [expr.prim.lambda.closure]/2.1 还指出闭包类型的大小和对齐方式是实现定义的,std::bit_cast因此根据[bit .cast]/2 :
返回: 类型的对象
To。隐式创建嵌套在结果中的对象 ([intro.object])。结果的值表示的每一位都等于 from 的对象表示中的对应位。结果的填充位未指定。对于结果和其中创建的每个对象,如果没有与所产生的值表示对应的对象类型的值,则行为是未定义的。如果有多个这样的值,则未指定生成哪个值。
然而,对于任何类型的实际用途,我认为如果一个构造具有以实现余地细节为条件的未定义行为(除非可以使用所述特征查询这些),那么应该合理地认为该构造具有未定义行为,除非可能对于编译器的内部 C++(例如 Clang 前端)实现,这些实现细节是已知的。
- @ user202729 确实,lambas 闭包大小的实现大小更改可能会使(看似)格式良好的程序(如 OP)变得可诊断格式错误,或者更糟的是,未定义。考虑到实现者实现 lambda 的闭包类型的余地,在这些类型的实例上使用诸如 `std::bit_cast` 之类的低级操作似乎本质上是危险的。如果我对上述的解释确实正确,即使在 Lamba 对象上使用 `std::bit_cast` 的(危险)练习可以是格式良好的,也是不合理的。