c++17 对于临时对象作为右值的优化
C++17 对临时对象作为右值的优化主要通过以下机制实现:
一、强制拷贝省略(Guaranteed Copy Elisation)
-
纯右值的直接构造
返回临时对象时,直接在调用方内存空间构造目标对象,无需调用拷贝/移动构造函数。纯右值的定义与特性
纯右值表示临时对象或字面量,属于无法取地址的短暂值。
在C++17前,纯右值返回需通过临时对象传递,可能触发拷贝/移动构造;C++17后,纯右值直接在目标地址构造,无需中间步骤
在变量初始化时,若初始值是纯右值(如函数返回的临时对象),则直接构造到目标变量中。
struct Widget {Widget(int) { std::cout << "构造\n"; }Widget(const Widget&) = delete; // 显式删除拷贝构造函数
};Widget func() { return Widget(42); // 纯右值:直接构造到调用方内存
}int main() {Widget w = func(); // 直接构造到w的内存,无拷贝/移动
}
输出
构造
只会调用一次构造函数,不会调用拷贝/移动构造函数,直接构造到w的内存,无拷贝/移动
二、临时物化(Temporary Materialization)
一、临时物化的定义与触发条件
-
定义
临时物化指纯右值(如字面量、函数返回的非引用对象)在需要实际存储时,隐式转换为将亡值(xvalue)的过程。 -
触发场景
- 绑定到右值引用:如
int&& r = 42;
,字面量42
(纯右值)需物化为临时对象以绑定到r
。 - 访问成员或子对象:如
std::string().c_str()
,std::string()
需物化为临时对象才能调用c_str()
。 - 初始化非类类型对象:如
void f(int&&); f(10);
,字面量10
需物化为 xvalue 以匹配参数类型。
- 绑定到右值引用:如
二、右值转换规则与移动语义
-
纯右值 → 将亡值
纯右值物化后成为 xvalue,此时允许移动语义介入(而非强制拷贝)。
示例:std::string createString() { return "hello"; } std::string s = createString(); // C++17 可能直接构造 `s`(无临时对象),否则移动临时对象
-
移动语义的触发
xvalue 可被右值引用绑定,从而调用移动构造函数或移动赋值运算符。
示例:std::vector<int> v1 = getVector(); // getVector() 返回纯右值,物化为 xvalue 后移动构造 `v1` std::vector<int> v2 = std::move(v1); // std::move(v1) 生成 xvalue,移动构造
三、C++17 的关键优化
-
延迟物化
纯右值仅在必要时物化,避免冗余临时对象。例如auto x = S{};
直接初始化x
,无需中间临时对象。 -
拷贝省略(Copy Elision)
当纯右值直接初始化对象时(如S s = S{};
),编译器可绕过物化直接构造目标对象(即 RVO/NRVO)。
四、典型示例分析
-
字面量的物化
int&& r = 42; // 字面量 `42` 物化为 xvalue,绑定到右值引用 `r`
-
函数返回值的优化
std::vector<int> getVec() { return {1, 2, 3}; } std::vector<int> v = getVec(); // 可能直接构造 `v`(无物化),或移动临时对象
-
移动语义的显式使用
std::string s1 = "temp"; std::string s2 = std::move(s1); // std::move(s1) 生成 xvalue,移动赋值给 `s2`
五、总结
- 临时物化是纯右值转换为可操作对象的桥梁,仅在需要时触发。
- 右值转换规则通过将纯右值提升为 xvalue,使移动语义高效介入。
- C++17 的优化(如延迟物化、拷贝省略)显著减少了冗余拷贝
三、结构化绑定优化
auto&& [a,b] = std::make_pair(1, 2.0); // 临时对象生命周期延长至整个作用域
结构化绑定声明中,临时对象的生命周期被完整保留,避免悬垂引用问题。
四、移动语义增强
-
STL 容器优化
标准库容器(如vector
、string
)在扩容时,对元素优先使用移动而非拷贝操作。std::vector<std::string> vec; vec.push_back("temporary"); // 临时string触发移动构造
-
返回值链式优化
嵌套函数返回的临时对象,可通过链式移动避免多次拷贝:std::string process() { return std::string("data").append("_processed"); } // 中间临时对象直接移动,无拷贝开销
优化效果对比(C++11 vs C++17)
场景 | C++11 | C++17 |
---|---|---|
返回不可拷贝对象 | 编译错误 | 允许构造 |
构造函数副作用 | 可能执行拷贝 | 强制省略拷贝 |
嵌套临时对象传递 | 多次移动 | 单次构造 |
这些改进使得 C++17 在处理临时对象时性能更接近理论最优值,同时减少了开发者手动优化的工作量
对比示例说明
示例
#include <iostream>
#include<utility>
class myComplex
{
private:double m_real{0.0};double m_imag{0.0};
public:explicit myComplex(const double r=0.0,const double i=0.0):m_real(r),m_imag(i){std::cout<<"usd construct\n";}myComplex(const myComplex&)=delete;myComplex(myComplex&& )=delete;myComplex& operator=(myComplex&&) =delete;myComplex& operator=(const myComplex&) =delete;myComplex operator+(const myComplex& myC) const {return myComplex(m_real + myC.m_real, m_imag + myC.m_imag);}myComplex& operator+=(const myComplex& mPc){m_real = mPc.m_real;m_imag = mPc.m_imag;return *this;}void myPrintf()const {std::cout<<"real = "<<m_real<<" imag = "<<m_imag<<std::endl;}
};int main()
{ std::cout<<__cplusplus<<std::endl;myComplex c1(1,3);myComplex c2(2,5);myComplex c3 = c1+c2;c3.myPrintf();std::cout<<"end \n";return 0;
}
编译运行 c++17
上述示例中 禁用了拷贝构造、拷贝赋值、移动构造和移动赋值,但 myComplex c3 = c1 + c2;
仍然可以编译执行
关键原因:返回值优化(RVO)(Return Value Optimization)
-
表达式解析
c1 + c2
会调用operator+
,它返回一个临时对象(prvalue)。理论上,这个临时对象需要通过拷贝/移动构造来初始化 ,但由于您禁用了这些操作,按理说应该报错。 -
RVO的介入
现代C++编译器(特别是启用优化时)会直接在c3
的内存位置上构造返回值,完全跳过拷贝/移动操作。这是标准允许的优化(C++17起强制要求)。 -
C++17的强制RVO
从C++17开始,对于纯右值(prvalue)的初始化,编译器必须省略拷贝/移动操作,即使这些操作被删除或不可访问。
编译c++14 或者c++11 报错
原因是c++14无优化 需要可用的拷贝/移动构造函数,不会强制拷贝省略。
修改示例
#include <iostream>
#include<utility>
class myComplex
{
private:double m_real{0.0};double m_imag{0.0};
public:explicit myComplex(const double r=0.0,const double i=0.0):m_real(r),m_imag(i){std::cout<<"usd construct\n";}// 拷贝构造函数myComplex(const myComplex& other) : m_real(other.m_real), m_imag(other.m_imag) {std::cout << "Copy constructor called\n";}// 移动构造函数myComplex(myComplex&& other) noexcept : m_real(std::exchange(other.m_real, 0)), m_imag(std::exchange(other.m_imag, 0)) {std::cout << "Move constructor called\n";}// 拷贝赋值运算符myComplex& operator=(const myComplex& other) {if (this != &other) { // 防止自赋值m_real = other.m_real;m_imag = other.m_imag;}std::cout << "Copy assignment called\n";return *this;}// 移动赋值运算符myComplex& operator=(myComplex&& other) noexcept {if (this != &other) { // 防止自赋值//...}std::cout << "Move assignment called\n";return *this;}myComplex operator+(const myComplex& myC) const { std::cout << "operator+ called\n";return myComplex(m_real + myC.m_real, m_imag + myC.m_imag);}myComplex& operator+=(const myComplex& mPc){ std::cout << "operator+= called\n";m_real = mPc.m_real;m_imag = mPc.m_imag;return *this;}void myPrintf()const {std::cout<<"real = "<<m_real<<" imag = "<<m_imag<<std::endl;}// myComplex(const myComplex&)=delete;// myComplex(myComplex&& )=delete;// myComplex& operator=(myComplex&&) =delete;// myComplex& operator=(const myComplex&) =delete;
};int main()
{ std::cout<<__cplusplus<<std::endl;myComplex c1(1,3);myComplex c2(2,5);myComplex c3 = c1+c2;c3.myPrintf();std::cout<<"end \n";return 0;
}
补充
NRVO
在 C++ 中,NRVO(Named Return Value Optimization,具名返回值优化)是编译器对函数返回具名局部对象时的一种优化技术,属于 RVO(返回值优化)的特殊情况。其核心目的是消除返回对象时的拷贝或移动开销,直接在调用方的内存位置构造目标对象。
NRVO 的核心特性
-
适用场景
- 函数返回一个具名的局部对象(非临时对象或全局变量)。
- 示例:
std::string createString() {std::string s = "Hello"; // 具名局部对象return s; // NRVO 可能优化 }
-
优化效果
- 跳过临时对象的构造和拷贝/移动操作,直接在调用方(如
main
函数中的接收变量)内存中构造返回值。 - 对比未优化代码(可能触发拷贝/移动构造函数),性能显著提升。
- 跳过临时对象的构造和拷贝/移动操作,直接在调用方(如
-
与 RVO 的区别
- RVO 优化匿名临时对象(如
return std::string("Hello");
),而 NRVO 优化具名对。 - NRVO 的实现复杂度更高,编译器支持程度可能因版本而异。
- RVO 优化匿名临时对象(如
NRVO 的触发条件
- 返回的局部对象类型必须与函数返回类型严格匹配(无类型转换)。
- 返回语句必须是局部对象的名称(如
return s;
),不能是表达式或条件分支。 - C++17 起,部分场景(如 URVO)强制优化,但 NRVO 仍为编译器可选优化。
示例与注意事项
// 触发 NRVO
myComplex func() {myComplex obj(1.0, 2.0); // 具名对象return obj; // 直接构造到调用方
}// 不触发 NRVO 的情况
myComplex func(bool flag) {myComplex a(1.0, 2.0), b(3.0, 4.0);return flag ? a : b; // 条件分支,无法优化:ml-citation{ref="6,7" data="citationList"}
}
总结
NRVO 通过消除不必要的拷贝/移动操作提升性能,但需依赖编译器的实现和代码的规范性。在资源管理类(如智能指针、容器)中,合理利用 NRVO 可显著减少动态内存操作的开销
RVO/NRVO 优化 c++11
C++11 标准明确支持 RVO(返回值优化) 和 NRVO(命名返回值优化),它们是编译器在特定场景下消除临时对象拷贝的关键优化技术。
1. RVO/NRVO 在 C++11 中的支持
- RVO:当函数返回匿名临时对象时,编译器可直接在调用者的存储空间构造该对象,避免拷贝/移动。
示例:myComplex operator+(...) { return myComplex(...); } // 匿名临时对象触发 RVO
- NRVO:当函数返回命名局部对象时,编译器可能直接将该对象替换为返回值,省略拷贝。
示例:myComplex func() { myComplex tmp; // 命名对象 return tmp; // 可能触发 NRVO }
2. C++11 与 C++17 的差异
- C++11:RVO/NRVO 是可选优化,编译器可根据实现决定是否应用。
- C++17:RVO 被强制要求(拷贝省略成为标准行为),但 NRVO 仍依赖编译器实现。
3. 优化条件与局限性
- 触发条件:
- 返回值类型与函数声明类型一致。
- 返回的局部对象非函数参数且非全局变量。
- 可能失效的场景:
- 返回语句包含条件分支(如
if
返回不同对象)。 - 对象具有移动构造函数(优先调用移动而非优化)。
- 返回语句包含条件分支(如
4. 验证优化效果
- 通过对比编译输出(
-O0
与-O2
)或检查汇编代码:g++ -O0 -fno-elide-constructors -S test.cpp # 禁用优化查看完整流程 g++ -O2 -S test.cpp # 启用优化观察省略效果
示例
#include <iostream>
#include<utility>
class myComplex
{
private:double m_real{0.0};double m_imag{0.0};
public:explicit myComplex(const double r=0.0,const double i=0.0):m_real(r),m_imag(i){std::cout<<"usd construct\n";}// 拷贝构造函数myComplex(const myComplex& other) : m_real(other.m_real), m_imag(other.m_imag) {std::cout << "Copy constructor called\n";}// 移动构造函数myComplex(myComplex&& other) noexcept { //......std::cout << "Move constructor called\n";}// 拷贝赋值运算符myComplex& operator=(const myComplex& other) {if (this != &other) { // 防止自赋值m_real = other.m_real;m_imag = other.m_imag;}std::cout << "Copy assignment called\n";return *this;}// 移动赋值运算符myComplex& operator=(myComplex&& other) noexcept {if (this != &other) { // 防止自赋值//...}std::cout << "Move assignment called\n";return *this;}myComplex operator+(const myComplex& myC) const { std::cout << "operator+ called\n";return myComplex(m_real + myC.m_real, m_imag + myC.m_imag);}myComplex& operator+=(const myComplex& mPc){ std::cout << "operator+= called\n";m_real = mPc.m_real;m_imag = mPc.m_imag;return *this;}void myPrintf()const {std::cout<<"real = "<<m_real<<" imag = "<<m_imag<<std::endl;}// myComplex(const myComplex&)=delete;// myComplex(myComplex&& )=delete;// myComplex& operator=(myComplex&&) =delete;// myComplex& operator=(const myComplex&) =delete;
};int main()
{ std::cout<<__cplusplus<<std::endl;myComplex c1(1,3);myComplex c2(2,5);myComplex c3 = c1+c2;c3.myPrintf();std::cout<<"end \n";return 0;
}
编译输出
代码调用了 两次移动构造函数,原因如下:
1. 临时对象的构造与传递过程
当禁用 RVO/NRVO(-fno-elide-constructors
)时,c1 + c2
的返回值处理会经历以下步骤15:
-
operator+
内部构造临时对象return myComplex(m_real + myC.m_real, m_imag + myC.m_imag); // 构造第一个临时对象(输出 `usd construct`)
- 第一次移动构造
将operator+
返回的临时对象移动到 函数返回值存储区(编译器生成的中间临时对象),触发第一次Move constructor called
。 - 第二次移动构造
将中间临时对象移动到c3
,触发第二次Move constructor called
。
2. 代码行为分解
myComplex c3 = c1 + c2;
具体执行流程:
步骤 | 行为 | 输出 |
---|---|---|
1 | operator+ 内联构造匿名临时对象 | usd construct |
2 | 匿名临时对象 → 返回值存储区(移动构造) | Move constructor called |
3 | 返回值存储区 → c3 (移动构造) | Move constructor called |
总结
特性 | C++11 支持情况 | 说明 |
---|---|---|
RVO | 支持(可选优化) | 适用于匿名临时对象15 |
NRVO | 支持(依赖编译器) | 适用于命名局部对象68 |
强制优化 | C++17 起对 RVO 强制要求 | NRVO 仍非强制8 |
c++版本查看
#include <iostream>int main() {std::cout << __cplusplus << std::endl;return 0;
}
199711 用于 C++98,
201103 用于 C++11
201402 用于 C++14
201703 用于 c++17
c++ 默认的函数
class MyClass {
public:// 默认构造函数MyClass() = default;// 默认拷贝构造函数MyClass(const MyClass&) = default;// 默认移动构造函数MyClass(MyClass&&) noexcept = default;// 默认拷贝赋值运算符MyClass& operator=(const MyClass&) = default;// 默认移动赋值运算符MyClass& operator=(MyClass&&) noexcept = default;// 析构函数~MyClass() = default;private:int data;std::string str;
};