Ранее я прочитал следующие ответы сегодня, и это было похоже на переизучение C ++, буквально.
Что такое семантика перемещения?
Что такое идиома копирования и обмена?
Тогда я подумал, стоит ли мне менять «способы» использования этих захватывающих функций; Основные проблемы, которые у меня возникают, касаются эффективности и ясности кода (первый для меня немного важнее, чем второй). Это привело меня к этому посту:
с чем я категорически не согласен (согласен с ответом, то есть); Я не думаю, что умное использование указателей может когда-либо сделать семантику перемещения избыточной, ни с точки зрения эффективности, ни с точки зрения ясности.
В настоящее время всякий раз, когда я реализую нетривиальный объект, я примерно делаю это:
struct Y
{
// Implement
Y();
void clear();
Y& operator= ( const& Y );
// Dependent
~Y() { clear(); }
Y( const Y& that )
: Y()
{
operator=(that);
}
// Y(Y&&): no need, use Y(const Y&)
// Y& operator=(Y&&): no need, use Y& operator=(const Y&)
};
Из того, что я понимаю из двух первых постов, которые я прочитал сегодня, я задаюсь вопросом, было бы полезно изменить это вместо этого:
struct X
{
// Implement
X();
X( const X& );
void clear();
void swap( X& );
// Dependent
~X() { clear(); }
X( X&& that )
: X()
{
swap(that);
// now: that <=> X()
// and that.~X() will be called shortly
}
X& operator= ( X that ) // uses either X( X&& ) or X( const X& )
{
swap(that);
return *this;
// now: that.~X() is called
}
// X& operator=(X&&): no need, use X& operator=(X)
};
Теперь, кроме того, что я немного сложнее и многословнее, я не вижу ситуации, в которой второе (struct X
) приведет к улучшению производительности, и я считаю, что она также менее читабельна. Предполагая, что мой второй код правильно использует семантику перемещения, как бы это улучшило мой нынешний «способ» делать вещи (struct Y
)?
Примечание 1: Единственная ситуация, которая, как мне кажется, проясняет последнее, заключается в «выход из строя»
X foo()
{
X automatic_var;
// do things
return automatic_var;
}
// ...
X obj( foo() );
для которого я думаю, что использование альтернативы std::shared_ptr
, а также std::reference_wrapper
если я устану get()
std::shared_ptr<Y> foo()
{
std::shared_ptr<Y> sptr( new Y() );
// do things
return sptr;
}
// ...
auto sptr = foo();
std::reference_wrapper<Y> ref( *ptr.get() );
только немного менее ясно, но так же эффективно.
Заметка 2: Я действительно приложил усилия, чтобы сделать этот вопрос точным и ответственным, и не подлежать обсуждению; пожалуйста продумайте это и не интерпретируйте как «Почему полезна семантика перемещения», это не что я спрашиваю
std::shared_ptr
хранит ваши данные в свободном хранилище (накладные расходы времени выполнения) и имеет потокобезопасный атомарный инкремент / декремент (накладные расходы времени выполнения) и может иметь значение null (либо игнорировать их и получать ошибки, либо постоянно проверять их на время выполнения и накладные расходы времени программиста), и имеет нетривиальный для прогнозирования времени жизни объекта (накладные расходы программиста).
Это ни в коем случае, форма или форма не так дешево, как move
,
Перемещение происходит в случае сбоя NRVO и других форм elision, поэтому, если у вас есть дешевое перемещение с использованием объектов в качестве значений, вы можете положиться на elision. Без дешевого движения полагаться на элиту опасно: на практике элиссия хрупка и не гарантируется стандартом.
Эффективное перемещение также делает контейнеры объектов эффективными без необходимости хранить контейнеры с умными указателями.
Уникальный указатель решает некоторые проблемы с разделяемым указателем, кроме принудительного свободного хранения и обнуляемости, а также блокирует простое использование конструкции копирования.
Кроме того, есть проблемы с вашим запланированным шаблоном, способным двигаться.
Во-первых, вам не нужно использовать конструкцию по умолчанию перед построением перемещения. Иногда конструкция по умолчанию не является бесплатной.
второй operator=(X)
не хорошо играет с некоторыми дефектами в стандарте. Я забыл, почему — вопрос о составе или наследовании? — Я постараюсь не забыть вернуться и отредактировать его.
Если конструкция по умолчанию почти бесплатна, а подкачка поэлементна, то здесь есть подход C ++ 14:
struct X{
auto as_tie(){
return std::tie( /* data fields of X here with commas */ );
}
friend void swap(X& lhs, X& rhs){
std::swap(lhs.as_tie(), rhs.as_tie());
}
X();// implement
X(X const&o):X(){
as_tie()=o.as_tie();
}
X(X&&o):X(){
as_tie()=std::move(o.as_tie());
}
X&operator=(X&&o)&{// note: lvalue this only
X tmp{std::move(o)};
swap(*this,o);
return *this;
}
X&operator=(X const&o)&{// note: lvalue this only
X tmp{o};
swap(*this,o);
return *this;
}
};
Теперь, если у вас есть компоненты, которые требуют ручного копирования (например, unique_ptr
) вышесказанное не работает. Я бы просто написал value_ptr
Я сам (то есть сказано, как копировать), чтобы скрыть эти детали от потребителей данных.
as_tie
функция также делает ==
а также <
(и связанные) легко написать.
Если X()
нетривиально, оба X(X&&)
а также X(X const&)
может быть написано вручную и эффективность восстановлена. И в качестве operator=(X&&)
это так коротко, иметь два из них не плохо.
Как альтернатива:
X& operator=(X&&o)&{
as_tie()=std::move(o.as_tie());
return *this;
}
это еще одна реализация =
это имеет свои плюсы (и то же самое для const&
). Это может быть более эффективным в некоторых случаях, но имеет худшую безопасность исключений. Это также устраняет необходимость swap
, но я бы ушел swap
в любом случае: поэлементный обмен того стоит.
В настоящее время всякий раз, когда я реализую нетривиальный объект, я примерно делаю это …
Я надеюсь, что вы откажетесь от этого, когда появятся более сложные элементы данных — например, типы, которые выполняют некоторую калибровку, генерацию данных, файловый ввод-вывод или получение ресурсов во время построения по умолчанию, только для того, чтобы быть выброшенными / освобожденными при (пере) назначении.
Я не вижу ситуации, в которой второй (
struct X
) даст улучшение производительности.
Тогда вы еще не понимаете семантику перемещения. Уверяю вас, такая ситуация существует. Но учитывая «Почему полезна семантика перемещения», я спрашиваю не об этом ». Я не собираюсь объяснять их вам снова здесь в контексте вашего собственного кода … иди «Пожалуйста, подумайте» сам. Если мышление снова не помогает, попробуйте добавить std::vector<>
ко многим МБ данных и тестам.
Добавление к тому, что сказали другие:
Вы не должны реализовывать конструктор, вызывая operator=
, Вы делаете это с:
Y( const Y& that ) : Y()
{
operator=(that);
}
Причина, по которой этого избежать, заключается в том, что требуется Y
(который даже не работает, если Y
не имеет конструктора по умолчанию); а также он будет бессмысленно создавать и уничтожать любые ресурсы, которые могут быть выделены конструктором по умолчанию.
Вы правильно исправили это для X
используя идиому копирования и замены, но затем вы вводите похожую ошибку:
X( X&& that ) : X()
{
swap(that);
Move-конструктор должен сооружать, не поменяться Опять же, бессмысленная конструкция по умолчанию, а затем уничтожение любых ресурсов, которые мог бы выделить конструктор по умолчанию.
Вам нужно будет написать конструктор перемещения для перемещения каждого члена. Это должно быть правильно, чтобы ваше унифицированное назначение копирования / перемещения работало.
Более общий комментарий: вы должны делать все это очень редко. Вот как вы создаете оболочку RAII для чего-то; ваши более сложные объекты должны быть сделаны из RAII-совместимых подобъектов, чтобы они могли следовать правилу нуля. В большинстве случаев существуют уже существующие обертки, такие как shared_ptr
так что вам не нужно писать свое.
Одним заметным улучшением является то, что функция «потребляет» или иным образом работает с параметром (т. Е. Принимает по значению, а не по ссылке). В этом случае без семантики перемещения передача некоторого lvalue в качестве параметра приведет к принудительному копированию.
Семантика перемещения дает вызывающей стороне возможность скопировать значение функции (несмотря на то, что она потенциально может быть дорогостоящей) или «переместить» значение в функцию, зная, что после этого вызова она больше не будет использоваться для переданного значения lvalue.
Кроме того, если функция использует shared_ptr (или другую оболочку, которая требует выделения) только для того, чтобы иметь возможность перемещать возвращаемое значение, использование семантики перемещения затем избавляет от этого выделения. Так что это больше, чем просто приятность, но и отличное повышение.
Хотя довольно просто написать код, который не использует семантику перемещения и сохраняет эффективность, использование семантики перемещения позволяет пользователям типа, который их поддерживает, иметь более простые интерфейсы и варианты использования (в основном всегда используя семантику значений).
РЕДАКТИРОВАТЬ:
Поскольку в OP специально указывается эффективность, стоит добавить, что семантика перемещения в лучшем случае может достичь той же эффективности, что и без них.
Насколько я знаю, для любого данного фрагмента кода, где добавление std :: move даст выигрыш в производительности по сравнению с отсутствием его, простая переработка кода может соответствовать лучшей эффективности перемещения или даже превзойти его.
Один недавний фрагмент кода, который я написал, был продюсером, который перебирал некоторые данные, выбирая и преобразовывая их, чтобы заполнить буфер для потребителя. Я написал небольшую уловку, которая абстрагировалась от сбора данных и передачи их потребителю.
Потребитель был либо однопоточным (т. Е. Обрабатывался в соответствии с тем, как проделал это производитель), либо другим потоком (стандартное однопоточное потребительское соглашение), либо многопоточным для платформы ПК.
В этом случае код производителя выглядел довольно аккуратно и просто, поскольку он заполнял контейнер и передавал его std :: move’d потребителю с помощью вышеупомянутого механизма, который был определен для каждой платформы.
Если предположить, что это можно сделать с тем же эффектом без использования семантики перемещения, это было бы совершенно правильно. Это просто позволило мне иметь — то, что я думал — был более простой код. Обычно цена, которую я бы заплатил, еще более грубая в определении объекта, но в данном случае это был стандартный контейнер, так что кто-то другой сделал эту работу;)