Реализовать оператор присваивания С++ с точки зрения конструктора

Фон

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

      // Store object on heap but keep value semantics
// Moved-from state has null elem; otherwise should have value
// Only valid operations on moved-from state are assignment and destruction
// Assignment only offers a basic exception guarantee
template <typename T>
class Box {
public:
    using value_type = T;

    // Default constructor
    Box() : elem{new value_type} {}
    // Accessor
    value_type& get() { return *elem; }
    value_type const& get() const { return *elem; }

    // Rule of Five
    Box(Box const& other) : elem{new value_type{*(other.elem)}} {};
    Box(Box&& other) : elem{other.elem}
    {
        other.elem = nullptr;
    };
    Box& operator=(Box const& other)
    {
        if (elem) {
            *elem = *(other.elem);
        } else {
            elem = new value_type{*(other.elem)};
        }
        return *this;
    }
    Box& operator=(Box&& other)
    {
        delete elem;
        elem = other.elem;
        other.elem = nullptr;
        return *this;
    }
    ~Box()
    {
        delete elem;
    }

    // Swap
    friend void swap(Box& lhs, Box& rhs)
    {
        using std::swap;
        swap(lhs.elem, rhs.elem);
    }
private:
    T* elem;
};

(Обратите внимание, что лучшая реализация будет иметь больше функций, таких как noexceptи функции, explicitперенаправление конструкторов на основе value_typeконструкторы ʼs, поддержка распределителей и т. д.; здесь я реализую только то, что необходимо для вопроса и тестов. Это также может сохранить некоторый код с помощью std::unique_ptr, но это сделало бы его менее наглядным примером)

Обратите внимание, что операторы присваивания совместно используют много кода друг с другом, с соответствующими конструкторами и с деструктором. Это было бы несколько уменьшено, если бы я не хотел разрешать присваивание перемещению из en, но это было бы более очевидно в более сложном классе.

Отступление: копирование и замена

Один из стандартных способов решения этой проблемы — использование идиомы копирования и замены (также называемой в данном контексте правилом четырех с половиной) , которая также дает вам надежную гарантию исключения, если nothrow:

          // Copy and Swap (Rule of Four and a Half)
    Box& operator=(Box other)  // Take `other` by value
    {
        swap(*this, other);
        return *this;
    }

Это позволяет написать только один оператор присваивания (взяв otherпо значению позволяет компилятору переместить другой параметр в параметр, если это возможно, и при необходимости выполняет копирование за вас), и этот оператор присваивания прост (при условии, что у вас уже есть swap). Однако, как говорится в связанной статье, у этого есть проблемы, такие как дополнительное выделение и хранение дополнительных копий содержимого во время операции.

Идея

Чего я раньше не видел, так это того, что я назову оператором присваивания «Уничтожить и инициализировать». Поскольку мы уже делаем всю работу в конструкторе, а версия объекта, для которой назначено, должна быть идентична версии, созданной путем копирования, почему бы не использовать конструктор следующим образом:

          // Destroy-and-Initialize Assignment Operator
    template <typename Other>
    Box& operator=(Other&& other)
    requires (std::is_same_v<Box, std::remove_cvref_t<Other>>)
    {
        this->~Box();
        new (this) Box{std::forward<Other>(other)};
        return *std::launder(this);
    }

Это по-прежнему делает дополнительное распределение, как это делает Copy-and-Swap, но только в случае назначения копирования вместо случая назначения перемещения, и оно делает это после уничтожения одной из копий T, поэтому он не дает сбоев при ограничении ресурсов.

Вопросы

  1. Предлагалось ли это раньше, и если да, то где я могу прочитать об этом подробнее?
  2. Является ли это UB в некоторых случаях, например, если Boxявляется подобъектом чего-то другого или разрешено уничтожать и перестраивать подобъекты?
  3. Есть ли у этого недостатки, о которых я не упомянул, например, constexprсовместимый?
  4. Существуют ли другие способы избежать повторного использования кода оператора присваивания, такие как этот и правило четырех с половиной, когда вы не можете просто = defaultих?

0 ответов

Другие вопросы по тегам