Вопросы об операторе назначения переезда - PullRequest
3 голосов
/ 24 марта 2012

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

struct A
{
    std::size_t s;
    int* p;
    A(std::size_t s) : s(s), p(new int[s]){}
    ~A(){delete [] p;}
    A(A const& other) : s(other.s), p(new int[other.s])
    {std::copy(other.p, other.p + s, this->p);}
    A(A&& other) : s(other.s), p(other.p)
    {other.s = 0; other.p = nullptr;}
    A& operator=(A const& other)
    {A temp = other; std::swap(*this, temp); return *this;}
    // Move assignment operator #1
    A& operator=(A&& other)
    {
        std::swap(this->s, other.s);
        std::swap(this->p, other.p);
        return *this;
    }
    // Move assignment operator #2
    A& operator=(A&& other)
    {
        delete [] p;
        s = other.s;
        p = other.p;
        other.s = 0;
        other.p = nullptr;
        return *this;
     } 
};

Вопрос:

Каковы преимущества и недостатки двух вышеперечисленных операторов назначения перемещения # 1 и # 2? Я полагаю, что единственное отличие, которое я вижу, состоит в том, что std::swap сохраняет хранилище lhs, однако я не понимаю, как это было бы полезно, поскольку значения в любом случае были бы уничтожены. Возможно, единственное время было бы с чем-то вроде a1 = std::move(a2);, но даже в этом случае я не вижу никакой причины использовать # 1.

Ответы [ 3 ]

8 голосов
/ 24 марта 2012

Это тот случай, когда вы действительно должны измерить.

И я смотрю на оператор присваивания copy ОП и вижу неэффективность:

A& operator=(A const& other)
    {A temp = other; std::swap(*this, temp); return *this;}

Чтоесли *this и other имеют одно и то же s?

Мне кажется, что более разумное назначение копирования могло бы избежать перехода в кучу, если s == other.s.Все, что нужно будет сделать, это копия:

A& operator=(A const& other)
{
    if (this != &other)
    {
        if (s != other.s)
        {
            delete [] p;
            p = nullptr;
            s = 0;
            p = new int[other.s];
            s = other.s;
        }
        std::copy(other.p, other.p + s, this->p);
    }
    return *this;
}

Если вам не нужно нужна строгая безопасность исключений, только базовая безопасность исключений при назначении копии (как std::string, std::vector и т. д.), то есть потенциальное улучшение производительности с учетом вышеизложенного.Сколько?Мера.

Я закодировал этот класс тремя способами:

Конструкция 1:

Используйте приведенный выше оператор назначения копирования и оператор назначения операции перемещения OP.# 1.

Конструкция 2:

Используйте вышеуказанный оператор назначения копирования и оператор назначения ОП для перемещения # 2.

Конструкция 3:

Оператор назначения копирования DeadMG для копирования и перемещения.

Вот код, который я использовал для проверки:

#include <cstddef>
#include <algorithm>
#include <chrono>
#include <iostream>

struct A
{
    std::size_t s;
    int* p;
    A(std::size_t s) : s(s), p(new int[s]){}
    ~A(){delete [] p;}
    A(A const& other) : s(other.s), p(new int[other.s])
    {std::copy(other.p, other.p + s, this->p);}
    A(A&& other) : s(other.s), p(other.p)
    {other.s = 0; other.p = nullptr;}
    void swap(A& other)
    {std::swap(s, other.s); std::swap(p, other.p);}
#if DESIGN != 3
    A& operator=(A const& other)
    {
        if (this != &other)
        {
            if (s != other.s)
            {
                delete [] p;
                p = nullptr;
                s = 0;
                p = new int[other.s];
                s = other.s;
            }
            std::copy(other.p, other.p + s, this->p);
        }
        return *this;
    }
#endif
#if DESIGN == 1
    // Move assignment operator #1
    A& operator=(A&& other)
    {
        swap(other);
        return *this;
    }
#elif DESIGN == 2
    // Move assignment operator #2
    A& operator=(A&& other)
    {
        delete [] p;
        s = other.s;
        p = other.p;
        other.s = 0;
        other.p = nullptr;
        return *this;
     } 
#elif DESIGN == 3
    A& operator=(A other)
    {
        swap(other);
        return *this;
    }
#endif
};

int main()
{
    typedef std::chrono::high_resolution_clock Clock;
    typedef std::chrono::duration<float, std::nano> NS;
    A a1(10);
    A a2(10);
    auto t0 = Clock::now();
    a2 = a1;
    auto t1 = Clock::now();
    std::cout << "copy takes " << NS(t1-t0).count() << "ns\n";
    t0 = Clock::now();
    a2 = std::move(a1);
    t1 = Clock::now();
    std::cout << "move takes " << NS(t1-t0).count() << "ns\n";
}

Вот вывод Igot:

$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=1  test.cpp 
$ a.out
copy takes 55ns
move takes 44ns
$ a.out
copy takes 56ns
move takes 24ns
$ a.out
copy takes 53ns
move takes 25ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=2  test.cpp 
$ a.out
copy takes 74ns
move takes 538ns
$ a.out
copy takes 59ns
move takes 491ns
$ a.out
copy takes 61ns
move takes 510ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=3  test.cpp 
$ a.out
copy takes 666ns
move takes 304ns
$ a.out
copy takes 603ns
move takes 446ns
$ a.out
copy takes 619ns
move takes 317ns

DESIGN 1 выглядит довольно хорошо для меня.

Предупреждение: Если у класса есть ресурсы, которые необходимо освободить "быстро", например, владение блокировкой мьютекса или открытие файла-обладание состояния, оператор присваивания дизайна-2 мог бы быть лучше с точки зрения правильности.Но когда ресурс представляет собой просто память, часто выгодно откладывать его освобождение как можно дольше (как в случае использования OP).

Предупреждение 2: Если у вас есть другие варианты использования, которые, как вы знаете, важны,измерить их.Вы можете прийти к другим выводам, чем я здесь.

Примечание: я ценю производительность выше, чем "СУХОЙ".Весь код здесь будет заключен в один класс (struct A).Сделайте struct A как можно лучше.И если вы выполняете достаточно качественную работу, то ваши клиенты struct A (которые могут быть вами) не будут испытывать искушение к "RIA" (Reinvent It Again).Я предпочитаю повторять небольшой код внутри одного класса, а не повторять реализацию целых классов снова и снова.

7 голосов
/ 24 марта 2012

Лучше использовать # 1, чем # 2, потому что если вы используете # 2, вы нарушаете DRY и дублируете свою логику деструктора.Во-вторых, рассмотрим следующий оператор присваивания:

A& operator=(A other) {
    swap(*this, other);
    return *this;
}

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

3 голосов
/ 24 марта 2012

Оператор присваивания, опубликованный DeadMG, делает все правильно, если swap() задействованные объекты не могут выбросить. К сожалению, это не всегда может быть гарантировано! В частности, если у вас есть распределители состояний, и это не сработает. Если распределители могут отличаться, кажется, что вам нужно отдельное назначение копирования и перемещения: конструктор копирования безоговорочно создаст копию, передаваемую в распределитель:

T& T::operator=(T const& other) {
    T(other, this->get_allocator()).swap(*this);
    return * this;
}

Назначение перемещения будет проверять, идентичны ли распределители и, если это так, просто swap() два объекта, а в противном случае просто вызывать назначение копирования:

T& operator= (T&& other) {
    if (this->get_allocator() == other.get_allocator()) {
        this->swap(other);
    }
    else {
        *this = other;
    }
    return *this;
}

Версия, принимающая значение, является гораздо более простой альтернативой, которая должна быть предпочтительной, если noexcept(v.swap(*this)) равен true.

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

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