Переместить семантику == пользовательская функция свопинга устарела?

Недавно много 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_; }