2012-03-23 4 views
3

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

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, однако я не вижу, как это было бы полезно, так как rvalues ​​были бы уничтожены в любом случае. Возможно, единственное время будет с чем-то вроде a1 = std::move(a2);, но даже в этом случае я не вижу причин использовать # 1.

+0

Я не совсем понимаю ваш вопрос. Почему бы вам просто не использовать 'std :: unique_ptr ' member (вместо 'int *') и иметь заданные операторы либо автоматически сгенерированные, либо '= default'? – Walter

+0

@Walter: Вопрос был учебным экспериментом, а не тем, что я буду использовать в производстве. Вместо этого я бы выбрал 'std :: vector'. Кроме того, на момент написания сообщения MSVC не реализовал 'default'. –

+0

Достаточно справедливо, но 'MSVC' не был среди тегов. – Walter

ответ

7

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

И я смотрю на оператора Ор в копию назначения и видя неэффективность:

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:

Используйте выше оператор копирующего присваивания и ФП в присваивание шаг оператор # 1.

Design 2:

Используйте выше оператор копирующего присваивания и ФП в присваивании движения оператор 2 #.

Design 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"; 
} 

Вот выход я получил:

$ 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 может быть лучше с точки зрения правильности. Но когда ресурс является просто памятью, часто бывает выгодно отложить его освобождение как можно дольше (как в случае использования ОП).

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

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

+0

Спасибо, это было очень информативно. Я также был удивлен результатом перехода в Design # 2. –

+2

Похоже, что преимущество в дизайне 1 над дизайном 2 обусловлено тем, что тестовая жгута не синхронизируется с вызовами деструктора - если вы включите это в свою синхронизацию, я ожидаю, что разница в производительности исчезнет. Я также немного удивлен тем, что LLVM не оптимизирует полностью a1 и a2. –

+0

Я решительно поддерживаю комментарий Ричарда Смита.Это время ** несправедливо ** и на самом деле не сравнивает одни и те же вещи. Область с тактовой частотой должна включать в себя уничтожение перемещенного объекта, поскольку на практике это почти всегда будет следовать за ходом. Я удивлен, что вы все этого не делали. – Walter

7

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

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

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

+0

Ради искателя, что такое СУХОЙ? –

+0

Спасибо, это был угол, который я не рассматривал. –

+1

DRY = «не повторяй себя» –

3

Оператор присваивания, отправленный 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.

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