Как перегрузить std::swap()

std::swap() используется многими контейнерами std (такими как std::list и std::vector) во время сортировки и даже присвоения.

Но std-реализация swap() очень обобщена и довольно неэффективна для пользовательских типов.

Таким образом, эффективность может быть достигнута путем перегрузки std::swap() с реализацией, специфичной для пользовательских типов. Но как реализовать ее так, чтобы она использовалась контейнерами std?

Комментарии к вопросу (2)
Решение

Правильный способ перегрузить swap - записать его в том же пространстве имен, что и то, что вы меняете, чтобы его можно было найти через argument-dependent lookup (ADL). Особенно легко это сделать следующим образом:

class X
{
    // ...
    friend void swap(X& a, X& b)
    {
        using std::swap; // bring in swap for built-in types

        swap(a.base1, b.base1);
        swap(a.base2, b.base2);
        // ...
        swap(a.member1, b.member1);
        swap(a.member2, b.member2);
        // ...
    }
};
Комментарии (17)

Внимание Mozza314

Вот моделирование эффектов универсального std :: algorithm, вызывающего std :: swap, и когда пользователь предоставляет свой своп в пространстве имен std. Поскольку это эксперимент, это моделирование использует namespace exp вместо namespace std.

// simulate 

#include 

namespace exp
{

    template 
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template 
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            exp::swap(begin[0], begin[1]);
    }

}

// simulate user code which includes 

struct A
{
};

namespace exp
{
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

Для меня это распечатывается:

generic exp::swap

Если ваш компилятор печатает что-то другое, то он неправильно реализует «двухфазный поиск» для шаблонов.

Если ваш компилятор соответствует (любому из C ++ 98/03/11), то он даст тот же вывод, который я показываю. И в этом случае именно то, чего вы боитесь, произойдет. И помещение вашего swap в пространство имен std (exp) не помешало этому случиться.

Дейв и я оба являемся членами комитета и работаем в этой области стандарта в течение десятилетия (и не всегда согласны друг с другом). Но этот вопрос был решен в течение длительного времени, и мы оба согласны с тем, как он был решен. Не обращайте внимания на экспертное мнение / ответ Дейва в этой области на свой страх и риск.

Этот выпуск стал известен после публикации C ++ 98. Начиная примерно с 2001 года мы с Дейвом начали работать в этой области. И это современное решение

// simulate 

#include 

namespace exp
{

    template 
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template 
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            swap(begin[0], begin[1]);
    }

}

// simulate user code which includes 

struct A
{
};

void swap(A&, A&)
{
    printf("swap(A, A)\n");
}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

Выход это:

swap(A, A)

Обновить

Было сделано наблюдение, что:

namespace exp
{    
    template 
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

работает! Так почему бы не использовать это?

Рассмотрим тот случай, когда ваш A является шаблоном класса:

// simulate user code which includes 

template 
struct A
{
};

namespace exp
{

    template 
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

Теперь это не работает снова. :-(

Таким образом, вы можете поместить swap в пространство имен std и заставить его работать. Но вам нужно помнить, чтобы поместить swap в пространство имен A для случая, когда у вас есть шаблон: A < T >. И поскольку оба случая будут работать, если вы поместите «swap» в пространство имен «A», просто легче запомнить (и научить других) просто сделать это одним способом.

Комментарии (18)

Вам не разрешается (по стандарту C++) перегружать std::swap, однако вам разрешено добавлять специализации шаблонов для ваших собственных типов в пространство имен std. Например.

namespace std
{
    template
    void swap(my_type& lhs, my_type& rhs)
    {
       // ... blah
    }
}

тогда использование в контейнерах std (и в любом другом месте) будет использовать вашу специализацию вместо общей.

Также обратите внимание, что реализация swap в базовом классе недостаточно хороша для ваших производных типов. Например, если у вас есть

class Base
{
    // ... stuff ...
}
class Derived : public Base
{
    // ... stuff ...
}

namespace std
{
    template
    void swap(Base& lha, Base& rhs)
    {
       // ...
    }
}

это будет работать для базовых классов, но если вы попытаетесь поменять местами два производных объекта, то будет использована общая версия из std, потому что шаблонизированный swap является точным совпадением (и это позволяет избежать проблемы замены только 'базовых' частей ваших производных объектов).

ПРИМЕЧАНИЕ: Я'обновил это, чтобы удалить неправильные биты из моего последнего ответа. D'oh! (спасибо puetzk и j_random_hacker за то, что указали на это).

Комментарии (9)

Хотя правильно, что обычно не следует добавлять что-либо в пространство имен std::, добавление специализаций шаблонов для определяемых пользователем типов разрешено. Перегрузка функций - нет. Это тонкая разница :-)

17.4.3.1/1 Для программы на C++ не определено добавлять объявления или определения в пространство имен std или пространства имен с пространством имен std, если не указано иное. если не указано иное. Программа может добавлять специализации шаблонов для любого стандартного библиотечного шаблона в пространство имен std. Такая специализация (полная или частичная) стандартной библиотеки приводит к неопределенному поведение, если только объявление не зависит от определяемого пользователем имени внешней связи и если специализация шаблона не удовлетворяет требованиям стандартной библиотеки для исходного шаблона.

Специализация std::swap будет выглядеть следующим образом:

namespace std
{
    template
    void swap(myspace::mytype& a, myspace::mytype& b) { ... }
}

Без бита template это была бы перегрузка, которая не определена, а не специализация, которая разрешена. Предлагаемый @Wilka' подход изменения пространства имен по умолчанию может сработать в пользовательском коде (из-за того, что Koenig lookup предпочтет версию без пространства имен), но это' не гарантировано, и на самом деле не должно (реализация STL должна использовать полностью квалифицированное std::swap).

Существует тема на comp.lang.c++.moderated с длинным обсуждением этой темы. Большая часть из них посвящена частичной специализации, хотя (в настоящее время нет хорошего способа сделать это).

Комментарии (6)