С++ 11 Оптимизация возвращаемого значения или перемещение?

Я не понимаю, когда я должен использовать std::move, и когда я должен позволить оптимизировать компилятор... например:

using SerialBuffer = vector< unsigned char >;

// let compiler optimize it
SerialBuffer read( size_t size ) const
{
    SerialBuffer buffer( size );
    read( begin( buffer ), end( buffer ) );
    // Return Value Optimization
    return buffer;
}

// explicit move
SerialBuffer read( size_t size ) const
{
    SerialBuffer buffer( size );
    read( begin( buffer ), end( buffer ) );
    return move( buffer );
}

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

Ответ 1

Используйте исключительно первый метод:

Foo f()
{
  Foo result;
  mangle(result);
  return result;
}

Это уже позволит использовать конструктор перемещения, если он доступен. Фактически, локальная переменная может привязываться к ссылке rvalue в инструкции return точно, когда разрешено копирование.

В вашей второй версии активно запрещается копирование. Первая версия является универсальной.

Ответ 2

Все возвращаемые значения либо уже moved, либо оптимизированы, поэтому нет необходимости явно перемещать с возвращаемыми значениями.

Составителям разрешено автоматически перемещать возвращаемое значение (для оптимизации копии) и даже оптимизировать ход!

Раздел 12.8 стандартного черновика n3337 (С++ 11):

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

[...]

Пример:

class Thing {
public:
Thing();
   ~Thing();
   Thing(const Thing&);
};

Thing f() {
   Thing t;
   return t;
}

Thing t2 = f();

Здесь критерии для элиции можно объединить, чтобы устранить два вызова конструктора копирования класса Thing: копирование локального автоматического объекта t во временный объект для возвращаемого значения функции f()и копирование этого временного объекта в объект t2. Фактически, построение локального объекта tможет рассматриваться как непосредственная инициализация глобального объекта t2, и что уничтожение объектов будет происходить в программе Выход. Добавление конструктора перемещения в Thing имеет тот же эффект, но это конструкция перемещения из временный объект до t2, который отклоняется. - конец примера]

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

Ответ 3

Это довольно просто.

return buffer;

Если вы это сделаете, то произойдет либо NRVO, либо нет. Если этого не произойдет, то buffer будет перемещен из.

return std::move( buffer );

Если вы это сделаете, NVRO не произойдет, а buffer будет перенесено из.

Таким образом, здесь ничего не выиграть, используя std::move здесь, и многое потерять.

Есть одно исключение из этого правила:

Buffer read(Buffer&& buffer) {
    //...
    return std::move( buffer );
}

Если buffer является ссылкой rvalue, вы должны использовать std::move. Это связано с тем, что ссылки не подходят для NRVO, поэтому без std::move это приведет к копированию с lvalue.

Это всего лишь экземпляр правила "всегда move ссылки rvalue и forward универсальные ссылки", который имеет приоритет над правилом "никогда move возвращаемое значение".

Ответ 4

Если вы возвращаете локальную переменную, не используйте move(). Это позволит компилятору использовать NRVO, и в противном случае компилятору по-прежнему будет разрешено выполнять перемещение (локальные переменные становятся значениями R в инструкции return). Использование move() в этом контексте просто блокирует NRVO и заставляет компилятор использовать перемещение (или копию, если перемещение недоступно). Если вы возвращаете что-то иное, чем локальная переменная, NRVO в любом случае не является вариантом, и вы должны использовать move(), если (и только если) вы намереваетесь похитить объект.