Ответ 1
Для типов, оператор копирования которых может перерабатывать ресурсы, замена с копией практически никогда не является лучшим способом реализации оператора присваивания копий. Например, посмотрите std::vector
:
Этот класс управляет буфером динамического размера и поддерживает как capacity
(максимальная длина, которую может хранить буфер), так и size
(текущая длина). Если реализован оператор присваивания vector
swap
, то независимо от того, какой новый буфер всегда выделяется, если rhs.size() != 0
.
Однако, если lhs.capacity() >= rhs.size()
, новый буфер не должен выделяться вообще. Можно просто назначить/построить элементы от rhs
до lhs
. Когда тип элемента тривиально можно копировать, это может сводиться только к memcpy
. Это может быть намного, намного быстрее, чем выделение и освобождение буфера.
Такая же проблема для std::string
.
Такая же проблема для MyType
, когда MyType
имеет члены данных, которые std::vector
и/или std::string
.
Есть только 2 раза, вы хотите рассмотреть возможность назначения копии с помощью swap:
-
Вы знаете, что метод
swap
(включая обязательную конструкцию копирования, когда rhs является lvalue) не будет ужасно неэффективным. -
Вы знаете, что вам всегда понадобится оператор присваивания копий, чтобы обеспечить надежную гарантию безопасности.
Если вы не уверены в 2, другими словами, вы полагаете, что оператору присваивания копии иногда может понадобиться сильная гарантия безопасности исключений, не реализуйте назначение с точки зрения обмена. Клиентам легко достичь такой же гарантии, если вы предоставите один из следующих вариантов:
- Отсутствие обмена.
- Оператор присваивания без перехвата.
Например:
template <class T>
T&
strong_assign(T& x, T y)
{
using std::swap;
swap(x, y);
return x;
}
или
template <class T>
T&
strong_assign(T& x, T y)
{
x = std::move(y);
return x;
}
Теперь будут некоторые типы, где реализация копирования с заменой будет иметь смысл. Однако эти типы будут исключением, а не правилом.
О:
void push_back(const value_type& val);
void push_back(value_type&& val);
Представьте vector<big_legacy_type>
где:
class big_legacy_type
{
public:
big_legacy_type(const big_legacy_type&); // expensive
// no move members ...
};
Если бы мы имели только:
void push_back(value_type val);
Тогда push_back
значение lvalue big_legacy_type
в vector
потребует 2 экземпляра вместо 1, даже если capacity
было достаточно. Это было бы катастрофой, с точки зрения производительности.
Обновление
Вот HelloWorld, который вы можете запустить на любой совместимой платформе С++ 11:
#include <vector>
#include <random>
#include <chrono>
#include <iostream>
class X
{
std::vector<int> v_;
public:
explicit X(unsigned s) : v_(s) {}
#if SLOW_DOWN
X(const X&) = default;
X(X&&) = default;
X& operator=(X x)
{
v_.swap(x.v_);
return *this;
}
#endif
};
std::mt19937_64 eng;
std::uniform_int_distribution<unsigned> size(0, 1000);
std::chrono::high_resolution_clock::duration
test(X& x, const X& y)
{
auto t0 = std::chrono::high_resolution_clock::now();
x = y;
auto t1 = std::chrono::high_resolution_clock::now();
return t1-t0;
}
int
main()
{
const int N = 1000000;
typedef std::chrono::duration<double, std::nano> nano;
nano ns(0);
for (int i = 0; i < N; ++i)
{
X x1(size(eng));
X x2(size(eng));
ns += test(x1, x2);
}
ns /= N;
std::cout << ns.count() << "ns\n";
}
Я закодировал X
оператор назначения копирования двумя способами:
- Неявно, что эквивалентно вызову оператора присваивания
vector
. - С идиомой copy/swap, предположительно под макросом
SLOW_DOWN
. Я думал о том, чтобы назвать егоSLEEP_FOR_AWHILE
, но этот способ на самом деле намного хуже, чем заявления сна, если вы находитесь на устройстве с батарейным питанием.
Тест конструирует некоторый случайный размер vector<int>
между 0 и 1000 и присваивает им миллион раз. Каждый раз, каждый раз, суммирует время, а затем находит среднее время в наносекундах с плавающей запятой и печатает это. Если два последовательных обращения к часам с высоким разрешением не возвращают что-то менее 100 наносекунд, вы можете увеличить длину векторов.
Вот мои результаты:
$ clang++ -std=c++11 -stdlib=libc++ -O3 test.cpp
$ a.out
428.348ns
$ a.out
438.5ns
$ a.out
431.465ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DSLOW_DOWN test.cpp
$ a.out
617.045ns
$ a.out
616.964ns
$ a.out
618.808ns
Я вижу 43% -ный выигрыш в производительности для идиомы copy/swap с помощью этого простого теста. YMMV.
Приведенный выше тест, в среднем, имеет достаточную пропускную способность в течение половины времени. Если мы возьмем это либо в крайнем случае:
- lhs имеет достаточную емкость все время.
- lhs имеет достаточную пропускную способность в любое время.
тогда преимущество в производительности присвоения копии по умолчанию по идиоме copy/swap варьируется от примерно 560% до 0%. Идиома копирования/свопа никогда не бывает быстрее и может быть значительно медленнее (для этого теста).
Хотите скорость? Измерить.