为何命名返回值优化(NRVO)在此场景下不生效?
我删除了GFXAPIImage类的拷贝构造函数,原本预期会触发命名返回值优化(NRVO)直接初始化对象,但编译报错。想请教两个问题:
- 是否必须声明拷贝构造函数?
- 如果必须声明,类成员
UniqueBufferPointer无法拷贝的问题该如何解决?
另外补充:如果给GFXAPIImage声明移动构造函数就能正常编译,这是不是说明编译器用了移动构造?但返回的是左值f,为什么会触发移动?
复现代码
class UniqueBufferPointer { public: UniqueBufferPointer() {} UniqueBufferPointer(const UniqueBufferPointer& other) = delete; UniqueBufferPointer(UniqueBufferPointer&& other) {} UniqueBufferPointer& operator=(const UniqueBufferPointer&) = delete; }; struct GFXAPIImage { GFXAPIImage() {} UniqueBufferPointer handle; GFXAPIImage(const GFXAPIImage&) = delete; //GFXAPIImage(const GFXAPIImage& other) = default; // 若使用此语句,编译器会报错无法访问UniqueBufferPointer的拷贝构造函数 }; GFXAPIImage func() { GFXAPIImage f; return f; //报错:GFXAPIImage(const GFXAPIImage&)无法被引用,它是已删除的函数 } int main() { GFXAPIImage f = func(); }
编译环境:Visual Studio,编译参数/O2
问题解答
核心原因
NRVO是编译器的可选优化,而非C++语言强制要求的行为。标准规定,无论编译器是否做NRVO,代码必须保证在不优化的场景下也能合法编译——这意味着类必须拥有可访问的拷贝构造或移动构造函数,作为优化失效时的后备路径。
你的代码中,GFXAPIImage显式删除了拷贝构造函数,同时因为显式声明(删除)了拷贝构造,编译器不会为它生成默认的移动构造函数(C++11及以后规则:若用户声明/删除了拷贝构造、拷贝赋值、析构中的任意一个,默认移动构造/赋值不会自动生成)。因此编译器找不到可用的拷贝或移动构造,直接报错。
具体问题解答
是否必须声明拷贝构造函数?
不需要。但必须保证类拥有可访问的拷贝构造或移动构造函数二者其一即可。NRVO不是强制的,编译器可能在某些场景下不触发优化(比如关闭优化开关),此时需要依赖拷贝或移动完成返回操作。成员
UniqueBufferPointer无法拷贝的解决办法UniqueBufferPointer是独占语义类型(拷贝被删除,仅支持移动),因此GFXAPIImage需要适配这种语义:显式声明并定义移动构造函数,调用成员的移动构造来转移资源:
struct GFXAPIImage { GFXAPIImage() {} UniqueBufferPointer handle; GFXAPIImage(const GFXAPIImage&) = delete; // 显式定义移动构造 GFXAPIImage(GFXAPIImage&& other) noexcept : handle(std::move(other.handle)) {} };
这样既符合UniqueBufferPointer的独占特性,又给编译器提供了合法的后备路径,同时NRVO依然可以正常触发(优化生效时不会实际调用移动构造)。
关于移动构造的补充疑问
当你声明移动构造后,编译器处理return f;时,会触发移动构造的隐式转换:C++标准规定,当返回的是函数内的局部对象时,即使是左值,编译器也允许将其视为右值来匹配移动构造——这是专门为返回值优化的后备场景设计的规则。简单来说,编译器会自动把return f;处理成return std::move(f);,因此能调用移动构造。
另外在/O2优化下,Visual Studio大概率会触发NRVO,此时移动构造不会被实际执行,只是代码必须保证它存在(作为优化失效的后备)。你可以在移动构造中添加打印语句验证:开启优化时不会输出日志,关闭优化时才会输出。
内容的提问来源于stack exchange,提问作者Zebrafish

