如何可能移动const返回的对象
How is moving a const returned object possible?
最近,我一直在阅读这篇文章和那篇建议停止返回const对象的文章。Stephan T.Lavavej在2013年《走向原住民》的演讲中也提出了这一建议。
我写了一个非常简单的测试来帮助我理解在所有这些情况下调用哪个构造函数/运算符:
- 返回常量或非常量对象
- 如果回归值优化(RVO)开始生效怎么办
- 如果命名返回值优化(NRVO)开始生效怎么办
以下是测试:
#include <iostream>
void println(const std::string&s){
try{std::cout<<s<<std::endl;}
catch(...){}}
class A{
public:
int m;
A():m(0){println(" Default Constructor");}
A(const A&a):m(a.m){println(" Copy Constructor");}
A(A&&a):m(a.m){println(" Move Constructor");}
const A&operator=(const A&a){m=a.m;println(" Copy Operator");return*this;}
const A&operator=(A&&a){m=a.m;println(" Move Operator");return*this;}
~A(){println(" Destructor");}
};
A nrvo(){
A nrvo;
nrvo.m=17;
return nrvo;}
const A cnrvo(){
A nrvo;
nrvo.m=17;
return nrvo;}
A rvo(){
return A();}
const A crvo(){
return A();}
A sum(const A&l,const A&r){
if(l.m==0){return r;}
if(r.m==0){return l;}
A sum;
sum.m=l.m+r.m;
return sum;}
const A csum(const A&l,const A&r){
if(l.m==0){return r;}
if(r.m==0){return l;}
A sum;
sum.m=l.m+r.m;
return sum;}
int main(){
println("build a");A a;a.m=12;
println("build b");A b;b.m=5;
println("Constructor nrvo");A anrvo=nrvo();
println("Constructor cnrvo");A acnrvo=cnrvo();
println("Constructor rvo");A arvo=rvo();
println("Constructor crvo");A acrvo=crvo();
println("Constructor sum");A asum=sum(a,b);
println("Constructor csum");A acsum=csum(a,b);
println("Affectation nrvo");a=nrvo();
println("Affectation cnrvo");a=cnrvo();
println("Affectation rvo");a=rvo();
println("Affectation crvo");a=crvo();
println("Affectation sum");a=sum(a,b);
println("Affectation csum");a=csum(a,b);
println("Done");
return 0;
}
以下是释放模式下的输出(带有NRVO和RVO):
build a
Default Constructor
build b
Default Constructor
Constructor nrvo
Default Constructor
Constructor cnrvo
Default Constructor
Constructor rvo
Default Constructor
Constructor crvo
Default Constructor
Constructor sum
Default Constructor
Move Constructor
Destructor
Constructor csum
Default Constructor
Move Constructor
Destructor
Affectation nrvo
Default Constructor
Move Operator
Destructor
Affectation cnrvo
Default Constructor
Copy Operator
Destructor
Affectation rvo
Default Constructor
Move Operator
Destructor
Affectation crvo
Default Constructor
Copy Operator
Destructor
Affectation sum
Copy Constructor
Move Operator
Destructor
Affectation csum
Default Constructor
Move Constructor
Destructor
Copy Operator
Destructor
Done
Destructor
Destructor
Destructor
Destructor
Destructor
Destructor
Destructor
Destructor
我不明白的是:为什么在"constructor csum"测试中使用move构造函数
返回对象是const,所以我真的觉得它应该调用复制构造函数。
我在这里缺少什么
它不应该是编译器的错误,VisualStudio和clang都给出了相同的输出。
我不明白的是:为什么在"constructor csum"测试中使用move构造函数?
在这种特殊情况下,允许编译器执行[N]RVO,但它没有执行。第二个最好的方法是移动构造返回的对象。
返回对象是const,所以我真的觉得它应该调用复制构造函数。
那根本不重要。但我想这并不完全明显,所以让我们来了解一下返回值在概念上意味着什么,以及[N]RVO是什么。为此,最简单的方法是忽略返回的对象:
T f() {
T obj;
return obj; // [1] Alternatively: return T();
}
void g() {
f(); // ignore the value
}
在标记为[1]的行中,存在从本地/临时对象到返回值的副本即使该值被完全忽略。这就是您在上面的代码中练习的内容。
如果您不忽略返回的值,如:
T t = f();
在概念上存在从返回值到CCD_ 1局部变量的第二副本。在你的所有案例中,第二个副本都被删除了。
对于第一个副本,返回的对象是否为const
并不重要,编译器根据[概念复制/移动]构造函数的参数来确定要做什么,而不是要构建的对象是否将为const
。这与相同
// a is convertible to T somehow
const T ct(a);
T t(a);
目标对象是否为const并不重要,编译器需要根据参数而不是目标找到最佳构造函数。
现在,如果我们回到您的练习中,为了确保没有调用复制构造函数,您需要修改return
语句的参数:
A force_copy(const A&l,const A&r){ // A need not be `const`
if(l.m==0){return r;}
if(r.m==0){return l;}
const A sum;
return sum;
}
这应该会触发副本构造,但同样简单的是,如果编译器认为合适,它可以完全删除副本。
根据我的观察,移动构造函数优先于复制构造函数。正如Yakk所说,由于存在多个返回路径,所以不能省略move构造函数。
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm#Copy%20vs%20Move
右值将更喜欢右值引用。左值会更喜欢左值参考文献。CV资格转换被认为是次要的相对于r/l值转换。右值仍然可以绑定到常量左值引用(const A&),但前提是没有更多重载集中有吸引力的右值引用。lvalues可以绑定到右值引用,但如果存在左值引用,则更喜欢它在过载集合中。更符合cv条件的对象不能绑定到一个不太合格的简历参考站。。。左值和右值引用。
在这一点上可以进行进一步的语言改进。什么时候从函数,应该有一个隐式转换为右值:
string operator+(const string& x, const string& y) { string result; result.reserve(x.size() + y.size()); result = x; result += y; return result; // as if return static_cast<string&&>(result); }
此隐式强制转换产生的逻辑导致从最好到最坏的"移动语义"层次结构:
If you can elide the move/copy, do so (by present language rules) Else if there is a move constructor, use it Else if there is a copy constructor, use it Else the program is ill formed
那么,如果您删除参数中的const &
呢?它仍然会调用move构造函数,但会为参数调用copy构造函数。如果您返回一个const对象呢?它将调用本地变量的复制构造函数。如果您退回const &
怎么办?它还将调用复制构造函数。
答案是,您的A sum
局部变量正在被移动到函数返回的const A
中(这是Move Constructor输出),然后编译器将从返回值复制到A acsum
中(因此没有copy Constructor输出)。
我分解了编译后的二进制文件(VC12发布版本,O2),得出的结论是:
t
0操作是在返回到堆栈分配的const A
临时对象之前将结果移动到csum(a,b)
内部,以用作以后A& operator=(const A&)
的参数。
move
操作不能使用move
cv限定变量,但在从csum
返回之前,sum
变量仍然是一个非常量变量,因此可以是moved
;并且需要是CCD_ 19以备返回后使用。
const
修饰符只是在返回后禁止编译器对move
执行操作,而不禁止csum
中的move
。如果从csum
中删除const
,结果将是:
Default Constructor Move Constructor Destructor Move Operator Destructor
顺便说一句,你的测试程序有一个错误,会导致a = sum(a, b);
不正确,a的默认ctor应该是:
A() : m(3) { println(" Default Constructor"); }
或者你会发现你的给定输出很难解释a = sum(a, b);
下面我将尝试分析调试构建ASM。结果是一样的。(分析发布版本就像自杀>_<)
main:
a = csum(a, b);
00F66C95 lea eax,[b]
00F66C98 push eax ;; param b
00F66C99 lea ecx,[a]
00F66C9C push ecx ;; param a
00F66C9D lea edx,[ebp-18Ch]
00F66CA3 push edx ;; alloc stack space for return value
00F66CA4 call csum (0F610DCh)
00F66CA9 add esp,0Ch
00F66CAC mov dword ptr [ebp-194h],eax
00F66CB2 mov eax,dword ptr [ebp-194h]
00F66CB8 mov dword ptr [ebp-198h],eax
00F66CBE mov byte ptr [ebp-4],5
00F66CC2 mov ecx,dword ptr [ebp-198h]
00F66CC8 push ecx
00F66CC9 lea ecx,[a]
00F66CCC call A::operator= (0F61136h) ;; assign to var a in main()
00F66CD1 mov byte ptr [ebp-4],3
00F66CD5 lea ecx,[ebp-18Ch]
00F66CDB call A::~A (0F612A8h)
csum:
if (l.m == 0) {
00F665AA mov eax,dword ptr [l]
00F665AD cmp dword ptr [eax],0
00F665B0 jne csum+79h (0F665D9h)
return r;
00F665B2 mov eax,dword ptr [r]
00F665B5 push eax ;; r pushed as param for
00F665B6 mov ecx,dword ptr [ebp+8]
00F665B9 call A::A (0F613F2h) ;; copy ctor of A
00F665BE mov dword ptr [ebp-4],0
00F665C5 mov ecx,dword ptr [ebp-0E4h]
00F665CB or ecx,1
00F665CE mov dword ptr [ebp-0E4h],ecx
00F665D4 mov eax,dword ptr [ebp+8]
00F665D7 jmp csum+0EEh (0F6664Eh)
}
if (r.m == 0) {
00F665D9 mov eax,dword ptr [r]
00F665DC cmp dword ptr [eax],0
00F665DF jne csum+0A8h (0F66608h)
return l;
00F665E1 mov eax,dword ptr [l]
00F665E4 push eax ;; l pushed as param for
00F665E5 mov ecx,dword ptr [ebp+8]
00F665E8 call A::A (0F613F2h) ;; copy ctor of A
00F665ED mov dword ptr [ebp-4],0
00F665F4 mov ecx,dword ptr [ebp-0E4h]
00F665FA or ecx,1
00F665FD mov dword ptr [ebp-0E4h],ecx
00F66603 mov eax,dword ptr [ebp+8]
00F66606 jmp csum+0EEh (0F6664Eh)
}
A sum;
00F66608 lea ecx,[sum]
A sum;
00F6660B call A::A (0F61244h) ;; ctor of result sum
00F66610 mov dword ptr [ebp-4],1
sum.m = l.m + r.m;
00F66617 mov eax,dword ptr [l]
00F6661A mov ecx,dword ptr [eax]
00F6661C mov edx,dword ptr [r]
00F6661F add ecx,dword ptr [edx]
00F66621 mov dword ptr [sum],ecx
return sum;
00F66624 lea eax,[sum]
00F66627 push eax ;; sum pushed as param for
00F66628 mov ecx,dword ptr [ebp+8]
00F6662B call A::A (0F610D2h) ;; move ctor of A (this one is pushed in main as a temp variable on stack)
00F66630 mov ecx,dword ptr [ebp-0E4h]
00F66636 or ecx,1
00F66639 mov dword ptr [ebp-0E4h],ecx
00F6663F mov byte ptr [ebp-4],0
00F66643 lea ecx,[sum]
00F66646 call A::~A (0F612A8h) ;; dtor of sum
00F6664B mov eax,dword ptr [ebp+8]
}
- 如何通过另一个对象中的命令正确地从一个对象返回数据
- 如何访问从 COM 对象返回的 VARIANT 数据类型中的安全数组C++?
- 从我的对象返回静态数组
- 将unique_ptr作为<Object>unique_ptr<常量对象返回>
- 从右值对象返回成员
- 视觉对象 返回 C++ 中的双精度值
- 为什么类型为 sf::Text 的对象返回不同的 getPosition().y 和 getLocalBounds().
- std::min_element 从类对象返回意外结果
- C++无效的对象返回语义
- const引用是否延长临时对象返回的临时对象的寿命
- 将 NULL 作为对象返回时未收到任何警告
- 如何在 C# 中从 com 对象返回数组(double[])
- 从重载运算符返回引用,并使用临时对象返回表达式
- Cin 对象返回值 c++
- 将变量作为类对象返回
- 使用可更改对象返回只读的最佳方法是什么
- 如何在Cython中从另一个包装对象返回包装的c++对象
- 当对象返回时,c++动态数组被清除
- 从带有动态字段的函数、对象返回
- 从堆栈上的匿名对象返回对*this的引用