You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++17 prvalue工作机制与拷贝消除相关技术咨询

问题解答

关于fun()示例中局部变量y的生命周期疑问

你对C17 prvalue机制的认知存在一个核心时序误区:C17重构值类别之后,prvalue本身不是一个已经构造完成的对象实体,而是一套“如何初始化目标对象”的规则描述。只有当prvalue遇到必须生成实体对象的场景(比如绑定到左值/右值引用、作为成员访问运算符的左操作数等),才会触发临时量实质化,生成一个实际的临时对象;其余场景下prvalue不会产生任何独立的临时对象,它的初始化逻辑会被直接传递到最终的构造目标上执行。

回到你给出的fun()示例:

class Y
{
public:
    Y() { cout << "Y dctor\n"; }
};

class X
{
public:
    X() { cout << "X dctor\n"; }
    X(const X&) { cout << "copy X\n"; }
    X(const Y&) { cout << "copy Y\n"; }
};

X fun()
{
    Y y;
    return X(y);
}

int main()
{
    X x1 = fun();
}

整个调用链中,X x1 = fun()里的x1就是所有prvalue最终要初始化的目标对象,不存在“fun的独立返回值对象”“X(y)生成的临时X对象”这类中间实体:

  • 执行到fun()内部的return语句时,fun的栈帧还未开始销毁,局部变量y仍处于存活状态
  • X(y)这个prvalue的构造逻辑不会在fun栈内生成临时X对象,而是直接在x1的内存地址上,以当前存活的y为参数调用X(const Y&)构造函数
  • 构造完成后,fun才会执行栈帧清理,销毁局部变量y并返回

整个过程不会产生任何X类型的临时对象,也不会发生拷贝,更不会访问已经销毁的y——因为对y的引用发生在构造x1的时刻,此时y还未离开作用域。你之前假设的“fun返回值先被X(y)初始化、fun结束销毁y、再用返回值构造x1”的两步时序,是C11/14未引入强制拷贝消除时的旧逻辑,在C17的prvalue规则下这个中间返回值实体根本不存在。


关于命名变量返回场景的C14/C17逻辑差异

你在参考资料中看到如下描述:

……命名返回值优化(NRVO)的相关规则没有任何变化。

这段描述是准确的,但这并不代表命名变量返回的代码在C14和C17下的执行逻辑完全一致,核心原因是强制拷贝消除和NRVO是两套互相独立的规则,二者的适用边界和强制力完全不同:

  • 强制拷贝消除(C++17新增):仅适用于prvalue初始化同类型对象的场景,是标准强制要求编译器必须执行的规则,不需要依赖拷贝/移动构造函数可访问,哪怕拷贝构造函数是delete的,只要符合场景代码就能编译通过。
  • NRVO:针对函数返回命名局部左值变量的场景,从C++98开始就存在,始终是标准允许但不强制的可选优化,且无论编译器是否真的优化掉拷贝调用,语义上都要求类的拷贝/移动构造函数可访问,否则代码无法编译。

回到你给出的g()/h()示例:

X g()
{
    X x;
    return x;
}

X h()
{
    return g();
}

int main()
{
    X x2 = h();
}

C++14下的执行逻辑

所有返回值的拷贝消除都是可选的优化行为,没有强制力:

  • 如果编译器完全关闭所有返回值优化,执行流程会产生3次X对象构造+3次拷贝:g内构造局部x → 拷贝x到g的返回值临时对象 → 拷贝g返回值到h的返回值临时对象 → 拷贝h返回值到x2,中间临时对象随作用域依次销毁
  • 主流编译器默认开启优化时通常会消除所有拷贝,但这种消除是编译器的可选行为,且全程要求X的拷贝/移动构造函数可访问。

C++17下的执行逻辑

规则分为两部分,其中NRVO部分和C++14完全一致,但prvalue传递部分有强制要求:

  1. 从g()返回值到h()返回值、再到main中x2的两层prvalue传递,属于强制拷贝消除的适用场景,编译器必须消除这两层的所有临时对象:也就是说g()的prvalue返回值、h()的prvalue返回值的初始化目标直接就是main中的x2,这两层不存在任何中间临时对象,也不需要这两步的拷贝/移动构造函数可访问,这是和C++14的核心差异。
  2. g()内部return x;的部分属于NRVO适用场景,规则和C++14完全一致:编译器可以选择做NRVO,直接把g内的局部x构造到x2的内存地址上,全程只调用一次X的默认构造函数,没有任何拷贝;编译器也可以选择不做NRVO,先在g的栈帧内构造局部x,再把x拷贝/移动到x2的内存地址,之后销毁局部x,这一步依然要求X的拷贝/移动构造函数可访问。

简单来说:C++17没有修改NRVO的规则,但把prvalue传递过程中的拷贝消除从“可选优化”变成了“强制要求”,哪怕编译器完全不做NRVO,g()返回值到h()返回值到x2的这两层拷贝也绝对不会出现。


内容的提问来源于stack exchange,提问作者CPPL

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 00:45:47