Если Idiom с копией и сменой становится Idiom Copy-and-Move в С++ 11?
Как объяснено в этом ответе, идиома копирования и свопинга реализована следующим образом:
class MyClass
{
private:
BigClass data;
UnmovableClass *dataPtr;
public:
MyClass()
: data(), dataPtr(new UnmovableClass) { }
MyClass(const MyClass& other)
: data(other.data), dataPtr(new UnmovableClass(*other.dataPtr)) { }
MyClass(MyClass&& other)
: data(std::move(other.data)), dataPtr(other.dataPtr)
{ other.dataPtr= nullptr; }
~MyClass() { delete dataPtr; }
friend void swap(MyClass& first, MyClass& second)
{
using std::swap;
swap(first.data, other.data);
swap(first.dataPtr, other.dataPtr);
}
MyClass& operator=(MyClass other)
{
swap(*this, other);
return *this;
}
};
Имея значение параметра MyClass как параметра для оператора =, параметр может быть сконструирован либо конструктором копирования, либо конструктором перемещения. Затем вы можете безопасно извлечь данные из параметра. Это предотвращает дублирование кода и помогает в безопасности исключений.
В ответе вы можете либо поменять, либо переместить переменные во временное. В первую очередь речь идет о замене. Однако обмен, если он не оптимизирован компилятором, включает в себя три операции перемещения, а в более сложных случаях делает дополнительную дополнительную работу. Когда все, что вам нужно, - переместить временный объект в назначенный объект.
Рассмотрим этот более сложный пример, включающий шаблон наблюдателя . В этом примере я написал код оператора присваивания вручную. Акцент делается на конструкторе перемещения, операторе присваивания и методе свопинга:
class MyClass : Observable::IObserver
{
private:
std::shared_ptr<Observable> observable;
public:
MyClass(std::shared_ptr<Observable> observable) : observable(observable){ observable->registerObserver(*this); }
MyClass(const MyClass& other) : observable(other.observable) { observable.registerObserver(*this); }
~MyClass() { if(observable != nullptr) { observable->unregisterObserver(*this); }}
MyClass(MyClass&& other) : observable(std::move(other.observable))
{
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
friend void swap(MyClass& first, MyClass& second)
{
//Checks for nullptr and same observable omitted
using std::swap;
swap(first.observable, second.observable);
second.observable->unregisterObserver(first);
first.observable->registerObserver(first);
first.observable->unregisterObserver(second);
second.observable->registerObserver(second);
}
MyClass& operator=(MyClass other)
{
observable->unregisterObserver(*this);
observable = std::move(other.observable);
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
}
Ясно, что дублируемая часть кода в этом ручном письменном операторе присваивания совпадает с дублирующей конструкцией перемещения. Вы могли бы выполнить обмен в операторе присваивания, и поведение было бы правильным, но оно потенциально могло бы выполнять больше ходов и выполнять дополнительную регистрацию (в свопе) и отмену регистрации (в деструкторе).
Разве не было бы лишнего смысла повторно использовать код конструктора перемещения?
private:
void performMoveActions(MyClass&& other)
{
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
public:
MyClass(MyClass&& other) : observable(std::move(other.observable))
{
performMoveActions(other);
}
MyClass& operator=(MyClass other)
{
observable->unregisterObserver(*this);
observable = std::move(other.observable);
performMoveActions(other);
}
Мне кажется, что этот подход никогда не уступает подходу подкачки. Правильно ли я считаю, что идиома с копией и заменой будет лучше, чем идиома копирования и перемещения в С++ 11, или я пропустил что-то важное?
Ответы
Ответ 1
Прошло много времени с тех пор, как я задал этот вопрос, и я знаю ответ некоторое время, но я отложил ответ на него. Вот оно.
Ответ - нет. Идиома "Копирование и своп" не должна становиться идиомой "Копировать и перемещать".
Важная часть Copy-and-swap (которая также является Move-construct-and-swap) является способом реализации операторов присваивания с безопасной очисткой. Старые данные заменяются на временные. Когда операция завершена, временная информация удаляется и вызывается ее деструктор.
Поведение подкачки позволяет повторно использовать деструктор, поэтому вам не нужно писать код очистки в операторы присваивания.
Если нет действий по очистке и только назначение, вы должны иметь возможность объявлять операторы присваивания по умолчанию, а копирование и своп не требуется.
Сам конструктор перемещения обычно не требует никакого поведения очистки, поскольку это новый объект. Общий простой подход заключается в том, чтобы заставить конструктор move вызвать конструктор по умолчанию, а затем поменять все элементы с объектом move-from. Затем перемещенный объект будет похож на объект, построенный по умолчанию.
Однако в этом примере шаблона наблюдателя вопрос, который на самом деле является исключением, когда вы должны выполнять дополнительную очистку, потому что ссылки на старый объект необходимо изменить. В общем, я бы рекомендовал делать ваши наблюдатели и наблюдаемые, а также другие конструктивные конструкции, основанные на ссылках, по возможности нецелесообразными.
Ответ 2
Прежде всего, вообще не нужно писать функцию swap
в С++ 11, пока ваш класс является подвижным. По умолчанию swap
будет применяться к перемещениям:
void swap(T& left, T& right) {
T tmp(std::move(left));
left = std::move(right);
right = std::move(tmp);
}
И что он, элементы меняются местами.
Во-вторых, на основе этого Copy-And-Swap по-прежнему сохраняется:
T& T::operator=(T const& left) {
using std::swap;
T tmp(left);
swap(*this, tmp);
return *this;
}
// Let not forget the move-assignment operator to power down the swap.
T& T::operator=(T&&) = default;
Будет либо копировать, либо заменять (что является движением), либо перемещать и свопировать (что является движением), и всегда должны приближаться к оптимальной производительности. Может быть несколько избыточных назначений, но, надеюсь, ваш компилятор позаботится об этом.
РЕДАКТИРОВАТЬ: это только реализует оператор копирования-назначения; требуется отдельный оператор присваивания переадресации, хотя он может быть установлен по умолчанию, иначе произойдет переполнение стека (переадресация и своп, вызывающие друг друга на неопределенный срок).
Ответ 3
Дайте каждому специальному участнику нежную любящую заботу, которую он заслуживает, и постарайтесь как можно больше по умолчанию:
class MyClass
{
private:
BigClass data;
std::unique_ptr<UnmovableClass> dataPtr;
public:
MyClass() = default;
~MyClass() = default;
MyClass(const MyClass& other)
: data(other.data)
, dataPtr(other.dataPtr ? new UnmovableClass(*other.dataPtr)
: nullptr)
{ }
MyClass& operator=(const MyClass& other)
{
if (this != &other)
{
data = other.data;
dataPtr.reset(other.dataPtr ? new UnmovableClass(*other.dataPtr)
: nullptr);
}
return *this;
}
MyClass(MyClass&&) = default;
MyClass& operator=(MyClass&&) = default;
friend void swap(MyClass& first, MyClass& second)
{
using std::swap;
swap(first.data, second.data);
swap(first.dataPtr, second.dataPtr);
}
};
При желании деструктор может быть неявным образом дефолтным. Все остальное должно быть явно определено или дефолтовано для этого примера.
Ссылка: http://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf
Идиома копирования/свопа, скорее всего, будет стоить вам производительности (см. слайды). Например, когда-нибудь задумывались, почему высокая производительность/часто используемые std:: types, такие как std::vector
и std::string
, не используют copy/swap? Плохая производительность является причиной. Если BigClass
содержит любые std::vector
или std::string
(что кажется вероятным), лучше всего называть их специальные члены из ваших специальных членов. Выше описано, как это сделать.
Если вам нужна сильная защита исключений при назначении, см. слайды о том, как предложить это в дополнение к производительности (поиск "strong_assign" ).