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

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

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

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

Затем, если вызывающий код хочет избежать копирования, он может передать значение r вместо значения 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 и скопировать elision, если смогу.

11

Решение

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

DataWrapperByValue(Data data);

Он принимает свой аргумент по значению, что означает, что в зависимости от того, является ли оно lvalue или rvalue, он будет вызывать data Копия параметра или Move-конструктор. В частности: если это lvalue, оно копируется. Если это значение, оно перемещается.

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

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

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

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

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

Ваш вывод о передаче по номеру ссылки вместе с версией по значению для случаев, когда вы получаете значение l, дает наилучшую производительность. Однако это широко считается преждевременной оптимизацией. Говард Хиннант (Лучший способ написать конструктор класса, который содержит контейнер STL в C ++ 11) и Шон Родитель (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, но это позволяет скрытые проблемы, такие как неявное преобразование с использованием явных конструкторов. Кроме того, без проверки типов аргументов преобразования откладываются внутри consttructor, где есть разные права доступа, разные ADL и т. Д., См. Живой пример: http://ideone.com/yb4e3Z

4

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

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. Этот создан с использованием данных (данных&&) конструктор. Затем второй конструктор перемещения вызывается во время конструктора DataWrapperByValue.

0