В последнее время я читаю эта почта а также этот пост предлагая прекратить возвращать const объекты.
Это предложение также дано Стефаном Т. Лававей в его разговор в Going Native 2013.
Я написал очень простой тест, чтобы помочь мне понять, какой конструктор / оператор вызывается во всех этих случаях:
Вот тест:
#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 выдают одинаковый результат.
Что я не понимаю, так это то, почему конструктор перемещения используется в тесте «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;
}
Это должно инициировать создание копии, но с другой стороны, все достаточно просто, чтобы компилятор мог полностью исключить копию, если сочтет ее подходящей.
Из того, что я заметил, конструктор перемещения имеет приоритет над конструктором копирования. Как говорит Якк, вы не можете исключить конструктор перемещения из-за нескольких путей возврата.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm#Copy%20vs%20Move
rvalues предпочтет ссылки на rvalue. lvalues предпочтет lvalue
Рекомендации. CV квалификации конверсии считаются вторичными
относительно преобразования r / l-значения. rvalues все еще может связываться с const
lvalue ссылка (const A&), но только если нет более
привлекательная ссылка в значении перегрузки. lvalues может связываться с
ссылка на значение, но предпочтительнее ссылка на значение, если она существует
в комплекте перегрузки. Правило, что более квалифицированный 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 &
? Это также вызовет конструктор копирования.
Ответ в том, что ваш 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]
}