Должен ли идиома «Копировать и поменять» идиома «Копировать и переместить» в C ++ 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 в качестве параметра для operator =, параметр может быть создан либо конструктором копирования, либо конструктором перемещения. Затем вы можете безопасно извлечь данные из параметра. Это предотвращает дублирование кода и способствует безопасности исключений.

В ответе упоминается, что вы можете поменять или переместить переменные во временную. В первую очередь речь идет об обмене. Однако обмен, если он не оптимизирован компилятором, включает три операции перемещения и в более сложных случаях выполняет дополнительную дополнительную работу. Когда все, что вы хотите, это переехать временный в назначенный объект.

Рассмотрим этот более сложный пример, включающий схема наблюдателя. В этом примере я написал код оператора присваивания вручную. Акцент делается на конструкторе перемещения, операторе присваивания и методе обмена:

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);
}

Мне кажется, что этот подход никогда не уступает своп-подходу. Правильно ли я считаю, что идиома копирования и обмена будет лучше, чем идиома копирования и перемещения в C ++ 11, или я упустил что-то важное?

22

Решение

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

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 :: типы, такие как std::vector а также std::string не использовать копирование / обмен? Причиной является низкая производительность. Если BigClass содержит любой std::vectorс или std::strings (что кажется вероятным), вам лучше всего позвонить их специальным членам из ваших специальных членов. Выше, как это сделать.

Если вам нужна строгая безопасность исключений при выполнении задания, см. Слайды, чтобы узнать, как предлагать это в дополнение к производительности (ищите «strong_assign»).

13

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

Прежде всего, как правило, нет необходимости писать swap функция в C ++ 11, пока ваш класс является подвижным. По умолчанию swap прибегнут к ходам:

void swap(T& left, T& right) {
T tmp(std::move(left));
left = std::move(right);
right = std::move(tmp);
}

И это все, элементы поменялись местами.

Во-вторых, основываясь на этом, копирование и замена на самом деле все еще имеет место:

T& T::operator=(T const& left) {
using std::swap;
T tmp(left);
swap(*this, tmp);
return *this;
}

// Let's not forget the move-assignment operator to power down the swap.
T& T::operator=(T&&) = default;

Будет либо копировать и менять местами (что является движением), либо перемещать и менять местами (что является движением), и всегда должно достигать почти оптимальной производительности. Может быть несколько избыточных назначений, но, надеюсь, ваш компилятор позаботится об этом.

РЕДАКТИРОВАТЬ: это только реализует оператор копирования-назначения; также требуется отдельный оператор перемещения-назначения, хотя он может быть установлен по умолчанию, в противном случае произойдет переполнение стека (перемещение-назначение и своп вызывают друг друга бесконечно).

13

Прошло много времени с тех пор, как я задавал этот вопрос, и я уже давно знаю ответ, но я отложил написание ответа на него. Вот.

Ответ — нет. Идиома «Копирование и замена» не должна становиться идиомой «Копирование и перемещение».

Важной частью Copy-and-swap (которая также является Move-construct-and-swap) является способ реализации операторов присваивания с безопасной очисткой. Старые данные обмениваются во временную структуру, созданную для копирования или перемещения. Когда операция завершена, временный объект удаляется, и вызывается его деструктор.

Поведение подкачки позволяет повторно использовать деструктор, поэтому вам не нужно писать какой-либо код очистки в ваших операторах присваивания.

Если нет необходимости выполнять очистку, а нужно только присваивать, то вы должны иметь возможность объявить операторы присваивания по умолчанию, а копирование и замена не нужны.

Сам конструктор перемещения обычно не требует каких-либо действий по очистке, поскольку это новый объект. Общий простой подход заключается в том, чтобы заставить конструктор перемещения вызывать конструктор по умолчанию, а затем поменять местами все элементы с объектом move-from. Перемещенный объект будет тогда похож на мягкий, сконструированный по умолчанию объект.

Тем не менее, в этом примере шаблона наблюдателя этого вопроса, на самом деле это исключение, когда вы должны выполнить дополнительную работу по очистке, потому что ссылки на старый объект должны быть изменены. В целом, я бы порекомендовал сделать ваши наблюдатели и наблюдаемые объекты, а также другие конструктивные конструкции, основанные на ссылках, неподвижными, когда это возможно.

4