Как возможно перемещение возвращаемого const объекта?
В последнее время я читал этот пост и этот пост, предлагая прекратить возвращать const объекты. Это предложение также дает Стефан Т. Лававей в своем выступлении в Going Native 2013.
Я написал очень простой тест, чтобы помочь мне понять, какой конструктор / оператор вызывается во всех этих случаях:
- Возвращение const или не const объектов
- Что будет, если начнется Оптимизация возврата ( 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"?
Возвращаемый объект const, поэтому я действительно чувствую, что он должен вызвать конструктор копирования.
Что мне здесь не хватает?
Это не должно быть ошибкой компилятора, так как Visual Studio и clang выдают одинаковый результат.
4 ответа
Что я не понимаю, так это то, почему конструктор перемещения используется в тесте "Constructor csum"?
В этом конкретном случае компилятору разрешено делать [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();
концептуально существует вторая копия возвращаемого значения в t
локальная переменная. Эта вторая копия проверяется во всех ваших делах.
Для первой копии, является ли возвращаемый объект const
или не имеет значения, компилятор определяет, что делать, основываясь на аргументах конструктора [концептуальное копирование / перемещение], а не на том, будет ли создаваемый объект const
или нет. Это так же, как:
// a is convertible to T somehow
const T ct(a);
T t(a);
Независимо от того, является ли объект назначения постоянным или нет, компилятору нужно найти лучший конструктор на основе аргументов, а не назначения.
Теперь, если мы вернем это к вашему упражнению, чтобы убедиться, что конструктор копирования не вызван, вам нужно изменить аргумент на 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;
}
Это должно инициировать создание копии, но с другой стороны, это достаточно просто, чтобы компилятор мог полностью исключить копию, если сочтет ее подходящей.
Ответ в том, что ваш A sum
локальная переменная перемещается в const A
возвращается функцией (это вывод конструктора перемещения), а затем копирует возвращаемое значение в A acsum
компилируется (поэтому нет вывода конструктора копирования).
Я разобрал скомпилированный двоичный файл (VC12, сборка выпуска, O2), и мой вывод:
move
операция состоит в том, чтобы переместить результат внутрь csum(a,b)
перед возвратом в стек, выделенный const A
временный объект, который будет использоваться в качестве параметра для последующего использования A& operator=(const A&)
,
move
операция не может move
cv-квалифицированная переменная, но до возврата из csum
, sum
переменная по-прежнему неконстантная переменная, поэтому может быть moved
; и нужно быть moved
для последующего использования после возврата.
const
Модификатор просто запрещает компилятору move
после возвращения, но не запрещает move
внутри csum
, Если вы удалите const
от csum
результат будет:
Default Constructor Move Constructor Destructor Move Operator Destructor
Кстати, ваша тестовая программа имеет ошибку, которая будет отображать a = sum(a, b);
неверно, ctor по умолчанию для A должен быть:
A() : m(3) { println(" Default Constructor"); }
Или вы найдете, что ваш вывод трудно объяснить для a = sum(a, b);
Ниже я попытаюсь проанализировать отладочную сборку ASM. Результат тот же. (Анализ выпуска релиза похож на самоубийство>_<)
главный:
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]
}
Из того, что я заметил, конструктор перемещения имеет приоритет над конструктором копирования. Как говорит Якк, вы не можете исключить конструктор перемещения из-за нескольких путей возврата.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm
rvalues предпочтет ссылки на rvalue. lvalues предпочтет ссылки на lvalue. Преобразования квалификации CV считаются вторичными относительно преобразований r/l-значения. Значения rvalue могут по-прежнему связываться с ссылкой const lvalue (const A&), но только если в наборе перегрузки нет более привлекательной ссылки rvalue. lvalues может связываться со ссылкой на rvalue, но предпочтет ссылку на lvalue, если она существует в наборе перегрузки. Правило, согласно которому более квалифицированный cv-объект не может связываться с менее квалифицированным cv-ссылкой, действует как для lvalue, так и для rvalue-ссылок.
На этом этапе может быть сделано дальнейшее уточнение языка. При возврате не квалифицированного cv объекта с автоматическим сохранением из функции должно быть неявное приведение к rvalue:
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 &
в параметрах? Он по-прежнему будет вызывать конструктор перемещения, но будет вызывать конструктор копирования для параметров. Что если вы вернете объект const? Он вызовет конструктор копирования для локальной переменной. Что делать, если вы вернете const &
? Это также вызовет конструктор копирования.