Законно ли реализовывать операторы присваивания как "destroy + construct"? - PullRequest
6 голосов
/ 08 октября 2019

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

При реализации оператора присваивания перемещения я предпочитаювызовите деструктор явным образом и на месте воссоздайте объект с размещением new. Таким образом, я избегаю дублирования логики деструктора. Кроме того, я часто реализую назначение копирования с точки зрения копирования + перемещения (когда это необходимо). Это приводит к следующему коду:

/** Canonical move-assignment operator. 
    Assumes no const or reference members. */
TYPE& operator = (TYPE && other) noexcept {
    if (&other == this)
        return *this; // self-assign

    static_assert(std::is_final<TYPE>::value, "class must be final");
    static_assert(noexcept(this->~TYPE()), "dtor must be noexcept");
    this->~TYPE();

    static_assert(noexcept(TYPE(std::move(other))), "move-ctor must be noexcept");
    new(this) TYPE(std::move(other));
    return *this;
}

/** Canonical copy-assignment operator. */
TYPE& operator = (const TYPE& other) {
    if (&other == this)
        return *this; // self-assign

    TYPE copy(other); // may throw

    static_assert(noexcept(operator = (std::move(copy))), "move-assignment must be noexcept");
    operator = (std::move(copy));
    return *this;
}

Мне кажется странным, но я не видел в Интернете никаких рекомендаций по реализации операторов присваивания перемещения + копирования таким «каноническим» способом. Вместо этого большинство веб-сайтов, как правило, реализуют операторы присваивания специфичным для типа способом, который необходимо вручную синхронизировать с конструкторами и деструктором при ведении класса.

Существуют ли какие-либо аргументы (помимо производительности) против реализацииперемещать и копировать операторы присваивания этим «независимым от типа» каноническим способом?

ОБНОВЛЕНИЕ 2019-10-08 на основе комментариев UB:

Я прочитал http://eel.is/c++draft/basic.life#8, чтокажется, чтобы покрыть рассматриваемый случай. Извлечение:

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

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

ОБНОВЛЕНИЕ 2019-10-10 на основе копии-and-swap comments:

Реализации присваивания могут быть объединены в один метод, который принимает аргумент-значение вместо ссылки. По-видимому, это также устраняет необходимость проверки static_assert и самостоятельного назначения. Моя новая предложенная реализация становится:

/** Canonical copy/move-assignment operator.
    Assumes no const or reference members. */
TYPE& operator = (TYPE other) noexcept {
    static_assert(!std::has_virtual_destructor<TYPE>::value, "dtor cannot be virtual");
    this->~TYPE();
    new(this) TYPE(std::move(other));
    return *this;
}

1 Ответ

4 голосов
/ 08 октября 2019

Существует сильный аргумент против вашей "канонической" реализации - это неправильно.

Вы заканчиваете время жизни исходного объекта и создаете новый объект вего место. Однако указатели, ссылки и т. Д. На исходный объект не обновляются автоматически, чтобы указывать на новый объект - вы должны использовать std::launder. (Это предложение неверно для большинства классов; см. Комментарий Дэвиса Херринга).) Затем деструктор автоматически вызывается для исходного объекта, вызывая неопределенное поведение .

Ссылка: (выделено мной) [class.dtor]/ 16

Как только деструктор вызывается для объекта, объект больше не существует;Поведение не определено, если деструктор вызывается для объекта, время жизни которого истекло. [ Пример: Если деструктор для автоматического объекта вызывается явно, и блок впоследствии остается таким образом, чтообычно вызывает неявное уничтожение объекта, поведение не определено. - конец примера ]

[basic.life] / 1

[...] Время жизниобъект o типа T заканчивается, когда:

  • , если T является типом класса с нетривиальным деструктором ([class.dtor]), начинается вызов деструктора, или

  • освобождается память, которую занимает объект, или повторно используется объектом, который не вложен в o ([intro.object]).

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


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


Фактический "канонический" способ заключается в использовании идиомы копирования и обмена :

// copy constructor is implemented normally
C::C(const C& other)
    : // ...
{
    // ...
}

// move constructor = default construct + swap
C::C(C&& other) noexcept
    : C{}
{
    swap(*this, other);
}

// assignment operator = (copy +) swap
C& C::operator=(C other) noexcept // C is taken by value to handle both copy and move
{
    swap(*this, other);
    return *this;
}

Обратите внимание, что здесь вам необходимо предоставитьпользовательская функция swap вместо использования std::swap, как упомянуто Говардом Хиннантом:

friend void swap(C& lhs, C& rhs) noexcept
{
    // swap the members
}

При правильном использовании копирование и замена не производят затрат, если соответствующие функцииправильно встроены (что должно быть довольно тривиально). Эта идиома очень часто используется, и среднестатистическому программисту на С ++ не должно быть проблем с ее пониманием. Вместо того, чтобы бояться, что это приведет к путанице, просто потратьте 2 минуты, чтобы изучить его, а затем использовать его.

На этот раз мы меняем значения объектов, и время жизни объекта не изменяется. Объект по-прежнему является исходным объектом, просто с другим значением, а не новым объектом. Подумайте об этом так: вы хотите, чтобы ребенок не запугивал других. Обмен значениями - это как гражданское воспитание, тогда как «уничтожение + конструирование» - это как убивание их , превращение их в временно мертвых и предоставление им совершенно нового мозга (возможно, спомощь магии). Последний метод может иметь некоторые нежелательные побочные эффекты, если не сказать больше.

Как и любая другая идиома, используйте ее, когда это уместно, - не просто используйте ее ради ее использования.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...