Постепенное значение, приводящее к дополнительному перемещению

Я пытаюсь понять семантику перемещения и скопировать/переместить elision.

Мне нужен класс, который обертывает некоторые данные. Я хотел бы передать данные в конструкторе, и я хотел бы иметь данные.

После чтения this, this и this У меня сложилось впечатление, что в С++ 11, если я хочу сохранить копию, тогда пропускная способность должна быть не менее эффективной, чем любая другая опция (кроме незначительная проблема увеличения размера кода).

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

Итак, я попробовал:

#include <iostream>

struct Data {
  Data()                 { std::cout << "  constructor\n";}
  Data(const Data& data) { std::cout << "  copy constructor\n";} 
  Data(Data&& data)      { std::cout << "  move constructor\n";}
};

struct DataWrapperWithMove {
  Data data_;
  DataWrapperWithMove(Data&& data) : data_(std::move(data)) { }
};

struct DataWrapperByValue {
  Data data_;
  DataWrapperByValue(Data data) : data_(std::move(data)) { }
};

Data
function_returning_data() {
  Data d;
  return d;
}

int main() {
  std::cout << "1. DataWrapperWithMove:\n"; 
  Data d1;
  DataWrapperWithMove a1(std::move(d1));

  std::cout << "2. DataWrapperByValue:\n";  
  Data d2;
  DataWrapperByValue a2(std::move(d2));

  std::cout << "3. RVO:\n";
  DataWrapperByValue a3(function_returning_data());
}

Вывод:

1. DataWrapperWithMove:
  constructor
  move constructor
2. DataWrapperByValue:
  constructor
  move constructor
  move constructor
3. RVO:
  constructor
  move constructor

Мне было приятно, что ни в одном из этих случаев не вызван конструктор копирования, но почему в втором случае есть дополнительный конструктор перемещения? Я предполагаю, что любой достойный конструктор перемещения для Data должен быть довольно быстрым, но он все еще меня пьет. У меня возникает соблазн использовать ссылку pass-by-rvalue-reference (первый вариант), так как это, по-видимому, приводит к одному вызову конструктора с меньшим числом движений, но я хотел бы использовать pass-by-value и copy elision, если можно.

Ответ 1

DataWrapperByValue имеет этот конструктор:

DataWrapperByValue(Data data);

Он принимает свой аргумент по значению, что означает, что в зависимости от того, является ли это значением lvalue или rvalue, он вызывается data параметром copy или move-constructor. В частности: если это значение lvalue, оно копируется. Если это значение r, оно перемещается.

Поскольку вы передаете rvalue через std::move(d2), конструктор перемещения вызывается для перемещения d2 в параметр. Второй вызов конструктора движения, конечно, осуществляется посредством инициализации элемента данных data_.

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

template<class U>
DataWrapperByValue(U&& u) : data_(std::forward<U>(u)) { }

Ответ 2

DataWrapperByValue::data_ перемещается из DataWrapperByValue::DataWrapperByValue(Data data) аргумента data, который перемещается из d2.

Ваше заключение к ссылочной ссылке с версией по значению для случаев, когда вы получаете l-значение, дает лучшую производительность. Однако это широко считается преждевременной оптимизацией. Howard Hinnant (Лучший способ написать конструктор класса, который содержит контейнер STL в С++ 11) и Sean Parent (http://channel9.msdn.com/Events/GoingNative/2013/Inheritance-Is-The-Base-Class-of-Evil) отметили, что они рассматривают эту преждевременную оптимизацию. Причина в том, что ходы должны быть недорогими, и их избежать в этом случае может привести к дублированию кода, особенно если у вас есть несколько аргументов, которые могут быть либо r, либо l-значением. Если при профилировании или тестировании вы обнаружите, что этот актуал снижает производительность, вы всегда можете легко добавить ссылку на обратную ссылку после факта.

Полезный шаблон в случае, когда вам нужна дополнительная производительность:

struct DataWrapperByMoveOrCopy {
  Data data_;
  template<typename T, 
    typename = typename std::enable_if<    //SFINAE check to make sure of correct type
        std::is_same<typename std::decay<T>::type, Data>::value
    >::type
  >
  DataWrapperByMoveOrCopy(T&& data) : data_{ std::forward<T>(data) } { }
};

здесь конструктор всегда делает правильные вещи, как можно увидеть в моем живом примере: http://ideone.com/UsltRA

Преимущество этого аргументированного сложного кода, вероятно, не имеет отношения к одному аргументу, но представьте, если бы у вашего конструктора было 4 аргумента, которые могли бы быть r или l-значениями, это намного лучше, чем писать 16 разных конструкторов.

struct CompositeWrapperByMoveOrCopy {
  Data data_;
  Foo foo_;
  Bar bar_;
  Baz baz_;
  template<typename T, typename U, typename V, typename W, 
    typename = typename std::enable_if<
        std::is_same<typename std::decay<T>::type, Data>::value &&
        std::is_same<typename std::decay<U>::type, Foo>::value &&
        std::is_same<typename std::decay<V>::type, Bar>::value &&
        std::is_same<typename std::decay<W>::type, Baz>::value
    >::type
  >
  CompositeWrapperByMoveOrCopy(T&& data, U&& foo, V&& bar, W&& baz) : 
  data_{ std::forward<T>(data) },
  foo_{ std::forward<U>(foo) },
  bar_{ std::forward<V>(bar) },
  baz_{ std::forward<W>(baz) } { }
};

Обратите внимание, что вы можете опустить проверку SFINAE, но это допускает такие тонкие проблемы, как неявное преобразование с использованием явных конструкторов. Кроме того, без проверки типов аргументов аргументы откладываются внутри конструктора, где существуют разные права доступа, разные ADL и т.д. См. Живой пример: http://ideone.com/yb4e3Z

Ответ 3

Я считаю, что это потому, что вы по существу делаете этот код.

std::cout << "2. DataWrapperByValue:\n";  
Data d2;
DataWrapperByValue a2(Data(std::move(d2))); // Notice a Data object is constructed. 

Примечание. DataWrapperByValue имеет только конструктор, который принимает значение lvalue. Когда вы выполняете std:: move (d2), вы передаете r-значение, поэтому будет создан другой объект Data для перехода к конструктору DataWrapperByValue. Он создается с использованием конструктора Data (Data &). Затем второй конструктор перемещения вызывается во время конструктора DataWrapperByValue.