Лучший способ реализовать идиому copy-and-swap в С++ 11

Я видел много правил реализации кода пять в терминах копирования и свопинга, но я думаю, что мы можем использовать функцию перемещения для замены функции свопинга, как в следующем коде:

#include <algorithm>
#include <cstddef>

class DumbArray {
public:
    DumbArray(std::size_t size = 0)
        : size_(size), array_(size_ ? new int[size_]() : nullptr) {
    }

    DumbArray(const DumbArray& that)
        : size_(that.size_), array_(size_ ? new int[size_] : nullptr) {
        std::copy(that.array_, that.array_ + size_, array_);
    }

    DumbArray(DumbArray&& that) : DumbArray() {
        move_to_this(that);
    }

    ~DumbArray() {
        delete [] array_;
    }

    DumbArray& operator=(DumbArray that) {
        move_to_this(that);
        return *this;
    }

private:
    void move_to_this(DumbArray &that) {
        delete [] array_;
        array_ = that.array_;
        size_ = that.size_;
        that.array_ = nullptr;
        that.size_ = 0;
   }

private:
    std::size_t size_;
    int* array_;
};

Этот код, я думаю,

  • Безопасное исключение
  • Требовать меньше ввода, так как многие функции просто вызовут move_to_this(), а назначение копирования и назначение переноса объединены в одну единственную функцию
  • Более эффективный, чем копирование и своп, поскольку своп включает в себя 3 назначения, а здесь всего 2, и этот код не страдает проблемами, упомянутыми в Эта ссылка

Я прав?

Спасибо

Edit:

  • Как отметил @Leon, может потребоваться выделенная функция для освобождения ресурса, чтобы избежать дублирования кода в move_to_this() и destructor
  • Как отметил @thorsan, для экстремальной производительности лучше отделить DumbArray& operator=(DumbArray that) { move_to_this(that); return *this; } до DumbArray& operator=(const DumbArray &that) { DumbArray temp(that); move_to_this(temp); return *this; } (благодаря @MikeMB) и DumbArray& operator=(DumbArray &&that) { move_to_this(that); return *this; }, чтобы избежать дополнительного перемещения operatoin

    После добавления отладочной печати я обнаружил, что в DumbArray& operator=(DumbArray that) {} не задействован дополнительный ход, когда вы называете это назначением перемещения

  • Как отметил @Erik Alapää, перед delete в move_to_this()

    необходима проверка самонаведения

Ответ 1

комментарии в строке, но кратко:

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

  • , если вы собираетесь определить пользовательский деструктор, сделайте это noexcept. Зачем открывать ящик пандоры? Я ошибся. По умолчанию это не исключено.

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

код:

#include <algorithm>
#include <cstddef>

class DumbArray {
public:
    DumbArray(std::size_t size = 0)
    : size_(size), array_(size_ ? new int[size_]() : nullptr) {
    }

    DumbArray(const DumbArray& that)
    : size_(that.size_), array_(size_ ? new int[size_] : nullptr) {
        std::copy(that.array_, that.array_ + size_, array_);
    }

    // the move constructor becomes the heart of all move operations.
    // note that it is noexcept - this means our object will behave well
    // when contained by a std:: container
    DumbArray(DumbArray&& that) noexcept
    : size_(that.size_)
    , array_(that.array_)
    {
        that.size_ = 0;
        that.array_ = nullptr;
    }

    // noexcept, otherwise all kinds of nasty things can happen
    ~DumbArray() // noexcept - this is implied.
    {
        delete [] array_;
    }

    // I see that you were doing by re-using the assignment operator
    // for copy-assignment and move-assignment but unfortunately
    // that was preventing us from making the move-assignment operator
    // noexcept (see later)
    DumbArray& operator=(const DumbArray& that)
    {
        // copy-swap idiom provides strong exception guarantee for no cost
        DumbArray(that).swap(*this);
        return *this;
    }

    // move-assignment is now noexcept (because move-constructor is noexcept
    // and swap is noexcept) This makes vector manipulations of DumbArray
    // many orders of magnitude faster than they would otherwise be
    // (e.g. insert, partition, sort, etc)
    DumbArray& operator=(DumbArray&& that) noexcept {
        DumbArray(std::move(that)).swap(*this);
        return *this;
    }


    // provide a noexcept swap. It the heart of all move and copy ops
    // and again, providing it helps std containers and algorithms 
    // to be efficient. Standard idioms exist because they work.
    void swap(DumbArray& that) noexcept {
        std::swap(size_, that.size_);
        std::swap(array_, that.array_);
    }

private:
    std::size_t size_;
    int* array_;
};

В операторе переадресации можно сделать еще одно повышение производительности.

Решение, которое я предложил, обеспечивает гарантию того, что перемещенный массив будет пустым (при освобождении ресурсов). Возможно, это не то, что вы хотите. Например, если вы отслеживали объем и размер DumbArray отдельно (например, например, std::vector), тогда вам может понадобиться сохранить любую выделенную память в this в that после перемещения. Тогда это позволит that присваиваться, возможно, уйти без выделения другой памяти.

Чтобы включить эту оптимизацию, мы просто реализуем оператор move-assign в терминах (noexcept) swap:

так из этого:

    /// @pre that must be in a valid state
    /// @post that is guaranteed to be empty() and not allocated()
    ///
    DumbArray& operator=(DumbArray&& that) noexcept {
        DumbArray(std::move(that)).swap(*this);
        return *this;
    }

:

    /// @pre that must be in a valid state
    /// @post that will be in an undefined but valid state
    DumbArray& operator=(DumbArray&& that) noexcept {
        swap(that);
        return *this;
    }

В случае с DumbArray, вероятно, стоит использовать более расслабленную форму на практике, но остерегайтесь тонких ошибок.

например.

DumbArray x = { .... };
do_something(std::move(x));

// here: we will get a segfault if we implement the fully destructive
// variant. The optimised variant *may* not crash, it may just do
// something_else with some previously-used data.
// depending on your application, this may be a security risk 

something_else(x);   

Ответ 2

Единственная (небольшая) проблема с вашим кодом - это дублирование функций между move_to_this() и деструктором, что является проблемой обслуживания, если ваш класс необходимо изменить. Конечно, это можно решить, извлекая эту часть в общую функцию destroy().

Моя критика "проблем", обсуждаемая Скоттом Мейерсом в его блоге:

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

  • предоставляет только оператор присваивания копии, который принимает свой аргумент по значению и
  • не потрудился написать оператор присваивания переадресации (именно то, что вы сделали).

Это автоматически решает проблему того, что ресурсы левого объекта будут заменены на правый объект и не будут немедленно выпущены, если правый объект не является временным.

Затем внутри реализации оператора присваивания копии в соответствии с идиомой копирования и свопинга swap() будет принимать в качестве одного из своих аргументов истекающий объект. Если компилятор может встроить деструктор последнего, то он, безусловно, устранит дополнительное назначение указателя - действительно, зачем сохранить указатель, который будет delete ed на следующем шаге?

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