Вопросы об операторе назначения переезда
Представьте себе следующий класс, который управляет ресурсом (мой вопрос касается только оператора присваивания перемещения):
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 ответа
Это тот случай, когда вы должны действительно измерить.
И я смотрю на оператора копирования копии ОП и вижу неэффективность:
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:
Используйте вышеупомянутый оператор назначения копирования и оператор назначения перемещения OP #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";
}
Вот результат, который я получил:
$ 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
Выглядит довольно хорошо для меня.
Предостережение: если у класса есть ресурсы, которые должны быть освобождены "быстро", такие как владение блокировкой мьютекса или владение открытым состоянием файла, оператор присваивания перемещения design-2 мог бы быть лучше с точки зрения корректности. Но когда ресурс представляет собой просто память, часто выгодно отложить освобождение его как можно дольше (как в случае использования OP).
Предостережение 2: Если у вас есть другие варианты использования, которые вы считаете важными, измерьте их. Вы можете прийти к другим выводам, чем я здесь.
Примечание: я оцениваю производительность выше "СУХОЙ". Весь код здесь будет инкапсулирован в одном классе (struct A
). Делать struct A
так хорошо, как вы можете. И если вы делаете достаточно качественную работу, то ваши клиенты struct A
(который может быть самим собой) не будет соблазн "RIA" (Reinvent It Again). Я предпочитаю повторять небольшой код внутри одного класса, а не повторять реализацию целых классов снова и снова.
Лучше использовать #1, чем #2, потому что если вы используете #2, вы нарушаете DRY и дублируете свою логику деструктора. Во-вторых, рассмотрим следующий оператор присваивания:
A& operator=(A other) {
swap(*this, other);
return *this;
}
Это как операторы копирования, так и перемещения без дублированного кода - отличная форма.
Оператор присваивания, опубликованный 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()
несоответствующие распределители, реализация, приведенная выше, является сильным исключением.