为何能在构造对象的Lambda中捕获未构造完成的对象?
为什么能捕获未构造的对象并读写?
先看这段代码:
int x = [&x](){ std::cout << "Inside lambda, x is " << x << '\n'; x = 5; return 23; }(); std::cout << "x is " << x << '\n';
它能编译运行且无任何警告,哪怕lambda里捕获并读写了还没完成初始化的x。
如果你觉得这是因为int可以默认构造,那试试这段自定义类型的代码:
struct Int { int v; Int(int v):v{v}{} }; static_assert(!std::is_constructible_v<Int>); static_assert(std::is_constructible_v<Int, int>); int main() { Int x = [&x](){ std::cout << "Inside lambda, x is " << x.v << '\n'; x = 5; return 23; }(); std::cout << "x is " << x.v << '\n'; }
这段代码同样能编译运行,这到底是怎么回事?
核心原因拆解
内存先分配,初始化后置
声明变量x时,编译器第一步就会为它分配好内存空间,之后才会执行右边的初始化表达式(也就是调用lambda)。所以lambda里捕获的&x,指向的是一块已经存在的内存地址——这步操作本身是符合C++规则的,标准允许获取尚未完成构造的对象的地址。这是标准明确的未定义行为
别被“能编译运行”的表象迷惑,这段代码其实踩了C++的红线:- 对于
int这类标量类型,读取未初始化的值属于未定义行为,编译器没有强制警告的义务; - 对于自定义的
Int类型,在构造函数执行完毕(对象生命周期正式开始)之前,访问它的成员v、执行赋值操作,同样是未定义行为。
- 对于
编译器没有报错或警告,是因为这种“初始化表达式里访问自身”的时序问题属于运行时逻辑范畴,编译器通常不会做这么深度的静态检查。现在能正常输出只是未定义行为的一种“幸运”表现,换个编译器、开启更高等级的优化或者换个运行环境,结果可能完全不可预料(比如输出垃圾值、程序崩溃)。
内容的提问来源于stack exchange,提问作者einpoklum
相关产品推荐
相关产品推荐

