c++中的copy elision是指一种编译器行为,即尽可能的消除不必要的拷贝构造和移动构造(当类可移动时则消除移动,不可移动时消除拷贝),包含以下几种情况:
- RVO (Return Value Optimization):当return之后的表达式为纯右值时,直接在函数调用的位置去构造返回值对象,省略掉拷贝/移动。 该行为在c++17视作编译器的优化,可以通过编译选项禁用掉;在c++17之后成为标准而非优化,即编译器在任何情况下都必须这么做,无法通过编译选项来禁用。
- NRVO (Named Return Value Optimization):当return之后的表达式为左值时,且该左值为该函数栈上的变量,则直接在函数调用的位置去构造返回值对象,省略掉拷贝/移动。 该行为是优化行为,可以通过编译选项来禁用。
- 初始化变量时,当初始化表达式为纯右值,则执行copy elision。
下面举个具体栗子:
#include <iostream>
struct A {
A() {
printf("%p ctor\n", this);
}
A(const A &) {
printf("%p copy ctor\n", this);
}
A(A &&) {
printf("%p move ctor\n", this);
}
~A() {
printf("%p dtor\n", this);
}
};
A f_RVO() {
return A();
}
A f_NRVO() {
A a;
printf("%p in func\n", &a);
return a;
}
int main() {
// A a = f_RVO();
A a = f_NRVO();
printf("%p in main\n", &a);
return 0;
}
我们写了一个可移动的类A,在构造时打印出它的地址;写了一个f_RVO()函数、一个f_NRVO()函数,也打印出相应的对象地址;用于演示copy elision在内存中是如何运作的。
① c++11,用-fno-elide-constructors禁用掉copy elision,观察f_RVO()函数:
0000005456dffa4f ctor
0000005456dffa9f move ctor
0000005456dffa4f dtor
0000005456dffa9e move ctor
0000005456dffa9f dtor
0000005456dffa9e in main
0000005456dffa9e dtor
输出如上 (提示:栈空间由高地址向低地址生长)
我们可以发现,首先在f_RVO()函数栈上(a4f地址)构造了一个对象,然后在main函数栈上开辟了一块临时地址(a9f)执行移动构造,再在main函数栈上真正要构造a的位置(a9e)执行移动构造。
共执行了1次构造+2次移动构造 (若类A不可移动则是1次构造+2次拷贝构造)。
② c++11,用-fno-elide-constructors禁用掉copy elision,观察f_NRVO()函数:
000000d6451ffa5f ctor
000000d6451ffa5f in func
000000d6451ffaaf move ctor
000000d6451ffa5f dtor
000000d6451ffaae move ctor
000000d6451ffaaf dtor
000000d6451ffaae in main
000000d6451ffaae dtor
现象基本同上。先在f_NRVO()函数栈上(a5f地址)构造了一个对象,然后在main函数栈上开辟了一块临时地址(aaf)执行移动构造,再在main函数栈上真正要构造a的位置(aae)执行移动构造。
③ c++17,用-fno-elide-constructors禁用掉copy elision,观察f_RVO()函数:
000000cd59dffc0f ctor
000000cd59dffc0f in main
000000cd59dffc0f dtor
由于在c++17中RVO是必须的,所以-fno-elide-constructors不管用了,直接在main函数栈上原地构造了。
④ c++17,用-fno-elide-constructors禁用掉copy elision,观察f_NRVO()函数:
000000bf551ff92f ctor
000000bf551ff92f in func
000000bf551ff97f move ctor
000000bf551ff92f dtor
000000bf551ff97f in main
000000bf551ff97f dtor
与c++11不同的是,在main函数栈上会少一次移动构造,不再有临时位置去构造一个临时对象了。
⑤ 开启优化,则原地构造。
000000fe2f3ff6ff ctor
000000fe2f3ff6ff in main
000000fe2f3ff6ff dtor
小测验:以下写法推荐吗?有什么问题?
vector<int> f() {
vector<int> v;
return std::move(v);
}
答案:
会阻止编译器做copy elision,因为std::move表达式是将亡值,它既不是左值也不是纯右值,所以既不能进行RVO也不能进行NRVO。 这种情况下就直接return v就好了,不要自作主张的加个std::move上去。。。
附栗子的全部结果
c++11 | c++11 -fno-elide-constructors | c++17 | c++17 -fno-elide-constructors | |
f_RVO() | 000000fe2f3ff6ff ctor 000000fe2f3ff6ff in main 000000fe2f3ff6ff dtor | 0000005456dffa4f ctor 0000005456dffa9f move ctor 0000005456dffa4f dtor 0000005456dffa9e move ctor 0000005456dffa9f dtor 0000005456dffa9e in main 0000005456dffa9e dtor | 0000000365fffbff ctor 0000000365fffbff in main 0000000365fffbff dtor | 000000cd59dffc0f ctor 000000cd59dffc0f in main 000000cd59dffc0f dtor |
f_NRVO() | 000000f72b1ff95f ctor 000000f72b1ff95f in func 000000f72b1ff95f in main 000000f72b1ff95f dtor | 000000d6451ffa5f ctor 000000d6451ffa5f in func 000000d6451ffaaf move ctor 000000d6451ffa5f dtor 000000d6451ffaae move ctor 000000d6451ffaaf dtor 000000d6451ffaae in main 000000d6451ffaae dtor | 000000ec881ffdff ctor 000000ec881ffdff in func 000000ec881ffdff in main 000000ec881ffdff dtor | 000000bf551ff92f ctor 000000bf551ff92f in func 000000bf551ff97f move ctor 000000bf551ff92f dtor 000000bf551ff97f in main 000000bf551ff97f dtor |