Как возможно перемещение возвращаемого const объекта?

В последнее время я читаю эта почта а также этот пост предлагая прекратить возвращать const объекты.
Это предложение также дано Стефаном Т. Лававей в его разговор в Going Native 2013.

Я написал очень простой тест, чтобы помочь мне понять, какой конструктор / оператор вызывается во всех этих случаях:

  • Возвращение const или не const объектов
  • Что если оптимизация возвращаемого значения (РВО) удары в ?
  • Что, если оптимизация именованных возвращаемых значений (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 выдают одинаковый результат.

7

Решение

Что я не понимаю, так это то, почему конструктор перемещения используется в тесте «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;
}

Это должно инициировать создание копии, но с другой стороны, все достаточно просто, чтобы компилятор мог полностью исключить копию, если сочтет ее подходящей.

4

Другие решения

Из того, что я заметил, конструктор перемещения имеет приоритет над конструктором копирования. Как говорит Якк, вы не можете исключить конструктор перемещения из-за нескольких путей возврата.

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 &? Это также вызовет конструктор копирования.

1

Ответ в том, что ваш A sum локальная переменная перемещается в const A возвращается функцией (это вывод конструктора перемещения), а затем копируется из возвращенного значения в A acsum компилируется (так что нет вывода Конструктора Копии).

1

Я разобрал скомпилированный двоичный файл (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]
}
1
По вопросам рекламы [email protected]