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

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

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

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

14.08.2008 19:24:17
Информация, связанная с данной
Pharap 4.05.2018 17:44:29
Swappable страница переехал в en.cppreference.com/w/cpp/named_req/Swappable
Andrew Keeton 29.11.2018 19:57:55
4 ОТВЕТА
РЕШЕНИЕ

Правильный способ перегрузить своп - это написать его в том же пространстве имен, что и своп, чтобы его можно было найти с помощью поиска, зависящего от аргумента (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);
        // ...
    }
};
132
13.02.2019 17:57:58
В C ++ 2003 это в лучшем случае недостаточно. Большинство реализаций используют ADL для поиска подкачки, но нет, это не обязательно, поэтому вы не можете на это рассчитывать. Вы можете специализировать std :: swap для конкретного конкретного типа, как показано в OP; просто не ожидайте, что эта специализация будет использоваться, например, для производных классов этого типа.
Dave Abrahams 1.06.2010 15:52:09
Я был бы удивлен, обнаружив, что реализации все еще не используют ADL, чтобы найти правильный обмен. Это старая проблема в комитете. Если ваша реализация не использует ADL для поиска подкачки, отправьте отчет об ошибке.
Howard Hinnant 23.02.2011 01:32:25
@ Саша: Во-первых, я определяю функцию в области имен, потому что это единственный вид определения, который имеет значение для общего кода. Потому что int et. и др. нет / не может иметь функции-члены, std :: sort et. и др. должны использовать бесплатную функцию; они устанавливают протокол. Во-вторых, я не знаю, почему вы возражаете против двух реализаций, но большинство классов обречены на неэффективную сортировку, если вы не можете согласиться с обменом не-членом. Правила перегрузки гарантируют, что если оба объявления будут видны, то будет выбрана более конкретная (эта), когда swap вызывается без уточнения.
Dave Abrahams 17.04.2011 14:24:36
@ Mozza314: Это зависит. A, std::sortкоторый использует ADL для замены элементов, не соответствует C ++ 03, но соответствует C ++ 11. Кроме того, почему -1 ответ, основанный на том факте, что клиенты могут использовать неидиоматический код?
JoeG 8.12.2011 13:10:05
@curiousguy: Если бы чтение стандарта было простым делом чтения стандарта, вы были бы правы :-). К сожалению, намерение авторов имеет значение. Таким образом, если исходное намерение состояло в том, что ADL можно или нужно использовать, это не указано. Если нет, то это просто старое критическое изменение для C ++ 0x, поэтому я написал «в лучшем случае» недостаточно точно.
Dave Abrahams 24.07.2013 23:09:37

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

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

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

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

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

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

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

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

53
27.01.2014 14:15:05
В основном хороший совет, но мне нужно -1 из-за тонкого различия, отмеченного puetzk между специализацией шаблона в пространстве имен std (что разрешено стандартом C ++) и перегрузкой (что не так).
j_random_hacker 11.03.2009 13:56:52
Проголосовал, потому что правильный способ настройки свопа - это сделать это в своем собственном пространстве имен (как указывает Дейв Абрахамс в другом ответе).
Howard Hinnant 23.02.2011 01:30:33
Мои причины для понижения голоса такие же, как у Говарда
Dave Abrahams 17.04.2011 14:26:10
@HowardHinnant, Дейв Абрахамс: я не согласен. На каком основании вы утверждаете, что ваша альтернатива - «правильный» путь? Как указано в стандарте puetzk, это специально разрешено. Хотя я новичок в этом вопросе, мне действительно не нравится метод, который вы защищаете, потому что если я определю Foo и поменяю местами таким образом, то кто-то, кто использует мой код, скорее всего, будет использовать std :: swap (a, b), а не swap ( а, б) на Foo, который молча использует неэффективную версию по умолчанию.
voltrevo 8.12.2011 12:39:18
@ Mozza314: ограничения по пространству и форматированию области комментариев не позволили мне ответить вам полностью. Пожалуйста, посмотрите ответ, который я добавил под названием «Внимание, Mozza314».
Howard Hinnant 8.12.2011 23:53:24

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

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

Специализация std :: swap будет выглядеть так:

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

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

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

29
20.09.2008 22:37:47
Одна из причин, по которой неправильно использовать специализацию шаблонов функций для этого (или чего-либо еще): он плохо взаимодействует с перегрузками, которых много для обмена. Например, если вы специализируете обычный std :: swap для std :: vector <mytype> &, ваша специализация не будет выбрана вместо стандартного вектора подкачки, потому что специализации не учитываются при разрешении перегрузки.
Dave Abrahams 24.02.2011 20:46:45
Это также то, что Мейерс рекомендует в Effective C ++ 3ed (пункт 25, стр. 106-112).
jww 5.09.2011 16:28:32
@DaveAbrahams: Если вы специализируетесь (без аргументов явных шаблонов), частичное упорядочение приведет к его быть специализация в в vectorверсии и будет использоваться .
Davis Herring 13.03.2018 13:54:26
@DavisHerring на самом деле, нет, когда вы делаете это частичное упорядочение не играет никакой роли. Проблема не в том, что вы не можете это назвать вообще; это то, что происходит при наличии явно менее специфичных перегрузок подкачки: wandbox.org/permlink/nck8BkG0WPlRtavV
Dave Abrahams 15.03.2018 15:20:16
@DaveAbrahams: частичное упорядочение - это выбор шаблона функции для специализации, когда явная специализация соответствует нескольким. Добавленная ::swapвами перегрузка является более специализированной, чем std::swapперегрузка vector, поэтому она захватывает вызов, и никакая специализация последнего не имеет значения. Я не уверен, насколько это практическая проблема (но я не утверждаю, что это хорошая идея!).
Davis Herring 15.03.2018 21:50:23

Внимание Моцца314

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

// simulate <algorithm>

#include <cstdio>

namespace exp
{

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

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

}

// simulate user code which includes <algorithm>

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 <algorithm>

#include <cstdio>

namespace exp
{

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

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

}

// simulate user code which includes <algorithm>

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 <algorithm>

template <class T>
struct A
{
};

namespace exp
{

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

}

// exercise simulation

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

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

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

70
31.08.2015 17:13:17
Большое спасибо за подробный ответ. Я явно менее осведомлен об этом, и мне было интересно, как перегрузка и специализация могут привести к другому поведению. Однако я не предлагаю перегрузку, а специализацию. Когда я добавляю template <>ваш первый пример, я получаю вывод exp::swap(A, A)из gcc. Так почему бы не предпочесть специализацию?
voltrevo 9.12.2011 00:26:37
Вот это да! Это действительно поучительно. Вы определенно убедили меня. Я думаю, что я немного изменю ваше предложение и использую синтаксис друзей в классе от Дейва Абрахамса (эй, я могу использовать это и для оператора << тоже! :-)), если у вас нет причин также избегать этого (кроме компиляции по отдельности). Кроме того, в свете этого, считаете ли вы using std::swapисключением из правила «никогда не помещайте операторы внутри заголовочных файлов»? На самом деле, почему бы не положить using std::swapвнутрь <algorithm>? Я полагаю, что это может нарушить ничтожную часть кода людей. Может быть, отказаться от поддержки и в конечном итоге положить его в?
voltrevo 9.12.2011 01:07:31
Синтаксис друга в классе должен быть в порядке. Я бы попытался ограничить using std::swapобласть действия в ваших заголовках. Да, swapэто почти ключевое слово. Но нет, это не совсем ключевое слово. Поэтому лучше не экспортировать его во все пространства имен, пока вам это не понадобится. swapочень похоже operator==. Самым большим отличием является то, что никто даже не думает о вызове operator==с определенным синтаксисом пространства имен (это было бы слишком уродливо).
Howard Hinnant 9.12.2011 01:17:55
@NielKirk: То, что вы видите как усложнение, - просто слишком много неправильных ответов. В правильном ответе Дейва Абрахамса нет ничего сложного: «Правильный способ перегрузить своп - это написать его в том же пространстве имен, что и свопинг, чтобы его можно было найти с помощью поиска, зависящего от аргумента (ADL)».
Howard Hinnant 2.10.2013 14:23:53
@codeshot: прости. Херб пытается донести это сообщение с 1998 года: gotw.ca/publications/mill02.htm Он не упоминает о свопе в этой статье. Но это всего лишь еще одно применение принципа интерфейса Херба.
Howard Hinnant 22.08.2015 21:20:32