Подтвердить что ты не робот

Использование std:: move() при возврате значения из функции, чтобы избежать копирования

Рассмотрим тип T, поддерживающий семантику перемещения по умолчанию. Также рассмотрите функцию ниже:

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

T o = f();

В старом С++ 03 некоторые неоптимальные компиляторы могут дважды вызвать конструктор копирования, один для "возвращаемого объекта" и один для o.

В С++ 11, поскольку t внутри f() является lvalue, эти компиляторы могут вызвать конструктор копирования как можно раньше, а затем вызвать конструктор перемещения для o.

Правильно ли утверждать, что единственный способ избежать первой "дополнительной копии" - переместить t при возврате?

T f() {
   T t;
   return std::move(t);
}
4b9b3361

Ответ 1

Нет. Всякий раз, когда локальная переменная в выражении return имеет право на копирование, она привязывается к rvalue re & shy; fe & shy; rence, и, таким образом, return t; идентичен return std::move(t); в вашем примере, в отношении того, какие конструкторы имеют право.

Обратите внимание, однако, что return std::move(t); не позволяет компилятору выполнять копирование, а return t; не является, и, следовательно, последний является предпочтительным стилем. [Благодаря @Johannes для кор и застенчивый; Rect и застенчивый;. Ион]. Если копия элизия бывает, вопрос о том, является ли или не двигаться строительство используется становится спорным

См. 12.8 (31, 32) в стандарте.

Обратите внимание также, что если T имеет доступный экземпляр, но удаленный конструктор move, то return t; не будет com & shy; pile, потому что сначала должен быть рассмотрен конструктор перемещения; вам нужно будет что-то сказать ef & shy; fect return static_cast<T&>(t);, чтобы заставить его работать:

T f()
{
    T t;
    return t;                 // most likely elided entirely
    return std::move(t);      // uses T::T(T &&) if defined; error if deleted or inaccessible
    return static_cast<T&>(t) // uses T::T(T const &)
}

Ответ 2

Нет. Лучшая практика - это непосредственно return t;.

В случае, если класс T имеет конструктор перемещения, который не удаляется, а уведомление T является локальной переменной, которая return t имеет право на копирование elision, она перемещает конструкцию возвращаемого объекта точно так же, как return std::move(t);. Однако return t; по-прежнему имеет право копировать/перемещать elision, поэтому конструкцию можно опустить, а return std::move(t) всегда создает возвращаемое значение с помощью конструктора перемещения.

В случае, если перемещение конструктора в классе T удаляется, но доступен конструктор копирования, return std::move(t); не будет компилироваться, а return t; все еще компилируется с использованием конструктора копирования. В отличие от @Kerrek, T не привязан к ссылке rvalue. Там есть двухступенчатое разрешение перегрузки для возвращаемых значений, подходящих для копирования, - попробуйте сначала перенести, затем скопируйте, и как перемещение, так и копирование, возможно, исчезли.

class T
{
public:
    T () = default;
    T (T&& t) = delete;
    T (const T& t) = default;
};

T foo()
{
    T t;
    return t;                   // OK: copied, possibly elided
    return std::move(t);        // error: move constructor deleted
    return static_cast<T&>(t);  // OK: copied, never elided
}

Если выражение return равно lvalue и не имеет права на копирование elision (скорее всего, вы возвращаете нелокальную переменную или выражение lvalue), и вы все равно хотите избежать копирования, то будет полезен std::move. Но имейте в виду, что наилучшая практика заключается в том, чтобы сделать возможным копирование.

class T
{
 public:
    T () = default;
    T (T&& t) = default;
    T (const T& t) = default;
};

T bar(bool k)
{
    T a, b;
    return k ? a : b;            // lvalue expression, copied
    return std::move(k ? a : b); // moved
    if (k)
        return a;                // moved, and possibly elided
    else
        return b;                // moved, and possibly elided
}

12.8 (32) в стандарте описывает процесс.

12.8 [class.copy]

32 Если критерии для выполнения операции копирования выполняются или выполняются, за исключением того факта, что исходный объект является параметром функции, а подлежащий копированию объект определяется значением lvalue, разрешением перегрузки для выбора конструктора для копии сначала выполняется так, как если бы объект был обозначен rvalue. Если сбой при перегрузке или если тип первого параметра выбранного конструктора не является ссылкой rvalue на тип объекта (возможно, с квалификацией cv), разрешение перегрузки выполняется снова, считая объект как lvalue. [Примечание. Это двухступенчатое разрешение перегрузки должно выполняться независимо от того, произойдет ли копирование. Он определяет вызывающий конструктор, если elision не выполняется, и выбранный конструктор должен быть доступен, даже если вызов отменяется. -end note]

Ответ 3

Хорошо, я хотел бы оставить комментарий по этому поводу. Этот вопрос (и ответ) заставил меня поверить, что нет необходимости указывать std::move в операторе return. Однако я просто подумал о другом уроке, имея дело с моим кодом.

Итак, у меня есть функция (она на самом деле специализируется), которая занимает временное и просто возвращает ее. (Шаблон общей функции выполняет другие функции, но специализация выполняет операцию идентификации).

template<>
struct CreateLeaf< A >
{
  typedef A Leaf_t;
  inline static
  Leaf_t make( A &&a) { 
    return a;
  }
};

Теперь эта версия вызывает конструктор копирования A при возврате. Если изменить оператор return на

Leaf_t make( A &&a) { 
  return std::move(a);
}

Затем вызывается конструктор перемещения A, и я могу сделать там некоторые оптимизации.

Возможно, это не соответствует 100% вашему вопросу. Но неверно думать, что return std::move(..) никогда не требуется. Я так и думал. Не больше; -)