C++ Clang下临时生命周期延长与复制消除合规性问询
C++17强制复制消除下聚合引用成员的临时对象生命周期分歧问题
场景说明
开发中可通过将包含引用成员的聚合类型设置为不可复制、不可移动,尝试以相对安全的方式基于聚合类型实现临时对象生命周期延长。但C++17引入的强制复制/移动消除规则不会校验对象是否具备可复制/可移动性,基于常规认知:复制消除场景下不会实际执行复制/移动操作,全程仅存在一个对象,该对象持有的引用应当将绑定的临时对象生命周期延长至聚合对象销毁时,但实际在不同编译器下出现了行为不一致。
复现代码
#include <iostream> struct K { K() { std::cout << "K::K()" << std::endl; } K(K const&) { std::cout << "K::K(K const&)" << std::endl; } K(K&&) { std::cout << "K::K(K&&)" << std::endl; } ~K() { std::cout << "K::~K()" << std::endl; } }; struct B { K const& l; ~B() { std::cout << "B::~B()" << std::endl; } }; int main() { B b = B{ K{} }; std::cout << "end of main" << std::endl; (void)b; }
注:示例中
B为可复制类型,即便将其修改为不可复制类型,运行结果完全一致。C17标准下,仅需将B的复制构造函数声明为delete即可让B不可复制且仍保持聚合类型属性;C20中该规则发生变更,需按P1008R1提案要求,在聚合类型中加入不可复制成员才能实现相同效果。
观察到的编译器行为差异
- MSVC、GCC:临时对象
K{}在变量b销毁后才执行析构 - Clang:临时对象
K{}在初始化表达式结束时就执行析构
问题解答
1. 上述代码是否触发未定义行为(UB)
该代码不触发未定义行为,所有行为差异均来自编译器对标准规则的实现分歧,不存在访问悬空引用等未定义语义。
2. 符合标准的正确实现是哪一方
Clang的实现严格匹配C++17正式标准的文本表述,GCC、MSVC的行为属于编译器提供的、符合用户直觉的实现扩展。
标准文本对临时对象生命周期延长的规则有明确边界:
- 当使用
T t = T{args}形式的拷贝初始化时,即便C++17强制复制消除保证T{args}直接在t的存储上构造,绑定到聚合内部引用成员的临时对象(即示例中的K{}),其生命周期默认仅持续到初始化所在的全表达式结束(即初始化语句执行完成时),不会自动延长到局部变量t的作用域结束。 - 只有使用直接初始化形式
B b{K{}}时,标准才明确要求绑定到聚合引用成员的临时对象生命周期延长至变量b的作用域结束,这种写法下三个编译器的行为完全一致。
之前的认知误区在于:强制复制消除仅省略了复制/移动构造函数的调用,不会自动修改临时对象的生命周期绑定规则,聚合内部引用绑定的临时量不会因为外层prvalue触发复制消除,就自动将生命周期附着到最终的局部变量上。
3. 该编译器行为分歧是否为已知问题
这是C++17强制复制消除落地后就存在的已知实现分歧:
- GCC和MSVC选择了更符合开发者直觉的实现逻辑:既然复制消除后初始化表达式的prvalue和局部变量是同一个对象,就将内部引用绑定的临时量生命周期同步延长到局部变量作用域结束,避免开发者无意义踩坑。
- Clang选择严格遵循标准文本的字面语义实现。
- 针对该规则歧义,C++标准委员会已经提交过对应的标准缺陷报告,明确未来标准版本中会要求复制消除场景下,聚合成员引用绑定的临时对象也需要延长生命周期到最终变量的作用域结束,但目前该缺陷修复的落地进度在不同编译器中存在差异。
内容的提问来源于stack exchange,提问作者TiredQuarenteneer
相关产品推荐
相关产品推荐

