Переместить семантику == пользовательская функция свопинга устарела?
Недавно много questions pop up о том, как предоставить свою собственную функцию swap
. С С++ 11 std::swap
будет использовать std::move
и переместить семантику, чтобы как можно быстрее обменять заданные значения. Это, конечно, работает только в том случае, если вы предоставляете конструктор перемещения и оператор присваивания перемещения (или тот, который использует pass-by-value).
Теперь, с учетом этого, действительно ли необходимо написать собственные функции swap
в С++ 11? Я мог думать только о недвижных типах, но опять же, пользовательский swap
обычно работает через какой-то "обмен указателями" (он же движется). Может быть, с некоторыми ссылочными переменными? Хм...
Ответы
Ответ 1
Это вопрос суждения. Я обычно позволяю std::swap
выполнять задание для кода прототипирования, но для кода выпуска пишут пользовательский своп. Обычно я могу написать собственный своп, который примерно в два раза быстрее, чем 1 перемещение конструкции + 2 назначения перемещения + 1 бесполезное уничтожение. Однако можно подождать до тех пор, пока std::swap
не станет проблемой производительности перед тем, как отправиться на работу.
Обновление для Альфа П. Штайнбаха:
20.2.2 [utility.swap] указывает, что std::swap(T&, T&)
имеет noexcept
эквивалент:
template <class T>
void
swap(T& a, T& b) noexcept
(
is_nothrow_move_constructible<T>::value &&
is_nothrow_move_assignable<T>::value
);
т.е. если операции перемещения на T
равны noexcept
, то std::swap
на T
есть noexcept
.
Обратите внимание, что эта спецификация не требует перемещения элементов. Для этого требуется только создание и назначение из rvalues, а если это noexcept
, тогда swap будет noexcept
. Например:.
class A
{
public:
A(const A&) noexcept;
A& operator=(const A&) noexcept;
};
std::swap<A>
не является исключением, даже без элементов перемещения.
Ответ 2
Могут быть некоторые типы, которые могут быть заменены, но не перемещены. Я не знаю каких-либо не подвижных типов, поэтому у меня нет примеров.
Ответ 3
По соглашению пользовательский swap
предлагает гарантию отсутствия броска. Я не знаю о std::swap
. Мое впечатление от комитета работает над тем, что это было все политическое, поэтому меня не удивило бы, если бы они где-то определили duck
как bug
, или аналогичные маневры в игровой манере. Поэтому я не стал бы полагаться на какой-либо ответ здесь, если только он не дает подробный удар от удара с С++ 0x к стандартному, с наименьшими деталями (чтобы быть уверенным, что нет bug
).
Ответ 4
Конечно, вы можете реализовать swap как
template <class T>
void swap(T& x, T& y)
{
T temp = std::move(x);
x = std::move(y);
y = std::move(temp);
}
Но у нас может быть наш собственный класс, скажем A
, который мы можем менять быстрее.
void swap(A& x, A& y)
{
using std::swap;
swap(x.ptr, y.ptr);
}
Что вместо запуска конструктора и деструктора просто заменяет указатели (которые вполне могут быть реализованы как XCHG или что-то подобное).
Конечно, компилятор может оптимизировать вызовы конструктора/деструктора в первом примере, но если у них есть побочные эффекты (т.е. вызовы на новый/удалить), он может быть недостаточно умен, чтобы оптимизировать их.
Ответ 5
Рассмотрим следующий класс, который содержит ресурс, выделенный памятью (для простоты, представленной одним целым числом):
class X {
int* i_;
public:
X(int i) : i_(new int(i)) { }
X(X&& rhs) noexcept : i_(rhs.i_) { rhs.i_ = nullptr; }
// X& operator=(X&& rhs) noexcept { delete i_; i_ = rhs.i_;
// rhs.i_ = nullptr; return *this; }
X& operator=(X rhs) noexcept { swap(rhs); return *this; }
~X() { delete i_; }
void swap(X& rhs) noexcept { std::swap(i_, rhs.i_); }
};
void swap(X& lhs, X& rhs) { lhs.swap(rhs); }
Затем std::swap
приводит к удалению нулевого указателя 3 раза (как для оператора присваивания перемещения, так и для объединения случаев оператора присваивания). У компиляторов может возникнуть проблема с оптимизацией такого delete
, см. https://godbolt.org/g/E84ud4.
Пользовательский swap
не вызывает никаких delete
и поэтому может быть более эффективным. Полагаю, именно по этой причине std::unique_ptr
предоставляет специализированную специализацию std::swap
.
UPDATE
Кажется, что компиляторы Intel и Clang могут оптимизировать удаление нулевых указателей, однако GCC - нет. См. Почему GCC не оптимизирует удаление нулевых указателей в С++? для деталей.
UPDATE
Кажется, что с помощью GCC мы можем запретить вызов оператора delete
путем перезаписи X
следующим образом:
// X& operator=(X&& rhs) noexcept { if (i_) delete i_; i_ = rhs.i_;
// rhs.i_ = nullptr; return *this; }
~X() { if (i_) delete i_; }