Подтвердить что ты не робот

Если 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, или я пропустил что-то важное?

4b9b3361

Ответ 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" ).