Вернуть копии значения вместо перемещения

Почему эта программа вызывает конструктор копирования вместо конструктора перемещения?

class Qwe {
public:
    int x=0;
    Qwe(int x) : x(x){}
    Qwe(const Qwe& q) {
        cout<<"copy ctor\n";
    }
    Qwe(Qwe&& q) {
        cout<<"move ctor\n";
    }    
};

Qwe foo(int x) {
    Qwe q=42;
    Qwe e=32;
    cout<<"return!!!\n";
    return q.x > x ? q : e;
}

int main(void)
{
    Qwe r = foo(50);
}

Результат:

return!!!
copy ctor

return q.x > x ? q : e; используется для отключения nrvo. Когда я завершаю его в std::move, он действительно перемещается. Но в "A Tour of С++" автор сказал, что переход c'tor следует вызывать, когда он доступен.

Что я сделал неправильно?

Ответы

Ответ 1

Вы не записывали свою функцию таким образом, чтобы разрешить копирование/перемещение. Требования к копии, подлежащей замене ходом, следующие:

[class.copy.elision]/3:

В следующих контекстах инициализации копирования операция перемещения может вместо операции копирования:

  • Если выражение в операторе return является (возможно, в скобках) id-выражением, которое называет объект с автоматическим срок хранения, указанный в теле или Параметр-объявление-предложение самой внутренней охватывающей функции или лямбда-выражения

разрешение перегрузки для выбора конструктора для копии сначала выполняются так, как если бы объект был обозначен rvalue. Если первый ошибка перегрузки не выполняется или не выполнялась, или если тип первый параметр выбранного конструктора не является ссылкой rvalue к типу объекта (возможно, с квалификацией cv), разрешение перегрузки выполняется снова, считая объект как lvalue.

Выше из С++ 17, но формулировка С++ 11 почти такая же. Условный оператор не является id-выражением, которое называет объект в области действия функции.

Идентификатор-выражение будет выглядеть как q или e в вашем конкретном случае. Вам нужно указать объект в этой области. Условное выражение не квалифицируется как именование объекта, поэтому оно должно преформировать копию.


Если вы хотите использовать свои способности к пониманию английского языка на сложной стене текста, то это как написано на С++ 11. Прилагает некоторые усилия, чтобы увидеть IMO, но это то же самое, что и уточненная версия выше:

Когда выполняются определенные критерии, реализация может пропустить копирование/перемещение объекта класса, даже если копирование/перемещение конструктор и/или деструктор объекта имеют побочные эффекты. [...] Это исключение операций копирования/перемещения, называемое копированием, является разрешено в следующих случаях (которые могут быть объединены с устранить несколько копий):

  • в операторе return в функции с типом возвращаемого класса, когда выражение является именем энергонезависимого автоматического объекта (другое чем параметр функции или catch-clause) с тем же cv-unqualified type как возвращаемый тип функции, копирование/перемещение операция может быть опущена путем непосредственного конструирования автоматического объекта в возвращаемое значение функции

Когда критерии для исключения операции копирования выполняются или будут сэкономленные за тот факт, что исходный объект является параметром функции, и подлежащий копированию объект обозначается значением lvalue, перегрузкой разрешение для выбора конструктора для копии сначала выполняется как будто объект был обозначен rvalue. Если разрешение перегрузки сбой, или если тип первого параметра выбранного конструктор не является ссылкой rvalue на тип объекта (возможно cv-qualified), разрешение перегрузки выполняется снова, учитывая объект как lvalue.

Ответ 2

Рассказчик не ответил на вопрос: почему переход c'tor не называется? (И нет: почему нет копии?)

Здесь мой ход: переход c'tor будет вызываться тогда и только тогда, когда:

  • Копирование elision (RVO) не выполняется. Ваше использование тернарного оператора действительно является способом предотвращения копирования. Позвольте мне отметить, что return (0, q); - это более простой способ сделать это, если вы просто хотите вернуть q при подавлении копирования. В этом случае используется (встроенный) оператор запятой. Возможно, return ((q)); тоже может работать, но мне недостаточно адвоката языка, чтобы точно сказать.
  • Аргумент return - это значение r. Это может быть временным (точнее, prvalue), но они также имеют право на копирование. Следовательно, аргумент return должен быть значением x, например std::move(q), если вы хотите, чтобы вызывался вызов c'tor.

См. также: Категории значений С++

Некоторые дополнительные особенности вашего примера:

  • q и e являются объектами типа Qwe.
  • q.x > x ? q : e - выражение lvalue типа Qwe. Это связано с тем, что выражения q и e являются lvalues ​​типа Qwe. Тернарный оператор просто выбирает любой из них.
  • std::move(q.x > x ? q : e) - выражение xvalue типа Qwe. std::move просто превращает (отличает) значение l в значение x. В стороне, q.x > x ? std::move(q) : std::move(e) также будет работать.
  • Копия c'tor вызывается в return q.x > x ? q : e;, потому что она может быть вызвана с lvalue типа Qwe (константа необязательна), а с другой стороны, перемещение c'tor не может быть вызвано с помощью lvalue и поэтому исключается из набора кандидатов.

UPDATE: обращение к комментариям путем углубления... это действительно запутанный аспект С++!

Концептуально, в С++ 98 возвращение объекта по значению означало возврат копии объекта, поэтому вызывается копия c'tor. Тем не менее, стандартные авторы считали, что компилятор должен быть свободен для выполнения оптимизации, чтобы эта потенциально дорогостоящая копия (например, контейнера) могла быть устранена при подходящих обстоятельствах.

Это копирование означает, что вместо создания объекта в одном месте и последующего копирования его на адрес памяти, управляемый вызывающим абонентом, вызывающий создает объект непосредственно в памяти, управляемой вызывающим. Следовательно, только "нормальный" конструктор, например. вызывается по умолчанию c'tor.

Поэтому они добавили такой проход, что компилятор должен проверить, что копия c'tor - независимо от того, сгенерирована ли она или определена пользователем - существует и доступна (в то же время не было понятия о удаленных функциях) и должен гарантировать, что объект инициализируется как-бы если он был сначала создан в другом месте и затем скопирован (см. правило as-if), но компилятор не должен был гарантировать, что любые побочные эффекты копии c'tor будут можно наблюдать, например, поток в вашем примере.

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

В С++ 11 была добавлена ​​семантика перемещения, и комитет очень хотел использовать ее таким образом, чтобы многие существующие функции возврата к значениям, например. использование струн или контейнеров станет более эффективным. Это было сделано таким образом, чтобы были предоставлены условия, в соответствии с которыми компилятору было фактически необходимо выполнить переход вместо копии. Однако идея копирования элиты оставалась важной, поэтому в основном существовали четыре разные категории:

  • Компилятор должен проверить работоспособность (см. выше), переместить c'tor, но разрешить его преодолеть.
  • Компилятор должен проверить работоспособный движок c'tor и должен его вызвать.
  • Компилятор должен проверять пригодную для использования копию c'tor, но ей разрешено удалять ее.
  • Компилятор должен проверять полезную копию c'tor и должен называть ее.

... которые, в свою очередь, приводят к четырем возможным результатам:

  • Компилятор проверяет перемещение c'tor, но затем удаляет его. (относится к 1. выше).
  • Компилятор проверяет перемещение c'tor и фактически выдает ему вызов. (относится к 1. или 2. выше).
  • Компилятор проверяет копию c'tor, но затем удаляет ее. (относится к 3. выше).
  • Компилятор проверяет копию c'tor и фактически выдает ему вызов. (относится к 3. или 4. выше).

И длинная история оптимизации здесь не заканчивается, потому что в С++ 17 компилятор должен избегать определенных вызовов c'tor. В этих случаях компилятору даже не разрешено требовать копирования или перемещения c'tor.

Обратите внимание, что компилятор всегда был свободен, чтобы избежать даже таких вызовов c'tor, которые не соответствуют стандартным требованиям, под защитой правила as-if, например, с помощью функции inline и следующих шагов оптимизации. Во всяком случае, вызов функции, концептуально, не обязательно должен поддерживаться фактической машинной инструкцией для выполнения подпрограммы. Компилятору просто не разрешено удалять наблюдаемое, иначе определенное поведение.

К настоящему моменту вы должны были заметить, что, по крайней мере, до С++ 17, очень хорошо, что одна и та же хорошо сформированная программа ведет себя по-разному, в зависимости от используемого компилятора и даже настроек оптимизации, если копия rsp, move имеет наблюдаемые побочные эффекты. Кроме того, компилятор, который реализует команду copy/move elision, может сделать это для подмножества условий, при которых стандарт позволяет это произойти. Это почти невозможно ответить на ваш вопрос. Почему здесь копируется/перемещается c'tor, но не так ли? Это может быть из-за требований стандарта С++, но также может быть предпочтительным для вашего компилятора. Возможно, авторы компилятора имели время и досуг, реализуя одну оптимизацию, но не другую. Может быть, они оказались слишком сложными в последнем случае. Может быть, у них просто было более важное занятие. Кто знает?

В 99% случаев для меня, как разработчика, нужно написать свой код таким образом, чтобы компилятор мог применять лучшие оптимизации. Одно дело - придерживаться обычных дел и стандартной практики. Знание NRVO и RVO временных ситуаций - это еще одна вещь и запись кода, который позволяет стандарту (или, на С++ 17, требуется) копировать/перемещать elision, и гарантировать, что перемещение c'tor доступно там, где это выгодно ( в случае если исключение не возникает). Не полагайтесь на побочные эффекты, такие как запись сообщения журнала или увеличение глобального счетчика. Это не то, что копия или перемещение c'tor обычно должны делать в любом случае, за исключением, возможно, для отладки или научных интересов.