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

115

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

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

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

Адам
источник
Связанная информация: en.cppreference.com/w/cpp/concept/Swappable
Pharap
Swappable страница переехал в en.cppreference.com/w/cpp/named_req/Swappable
Эндрю Китон

Ответы:

135

Правильный способ перегрузки свопа - записать его в том же пространстве имен, что и то, что вы обмениваете, чтобы его можно было найти с помощью поиска, зависящего от аргументов (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);
        // ...
    }
};
Дэйв Абрахамс
источник
11
В C ++ 2003 он в лучшем случае недооценен. Большинство реализаций действительно используют ADL для поиска подкачки, но это не обязательно, так что вы не можете рассчитывать на это. Вы можете специализировать std :: swap для определенного конкретного типа, как показано OP; просто не ждите, что эта специализация будет использоваться, например, для производных классов этого типа.
Dave Abrahams
15
Я был бы удивлен, обнаружив, что реализации по- прежнему не используют ADL для поиска правильного свопа. Это старый вопрос комитета. Если ваша реализация не использует ADL для поиска подкачки, отправьте отчет об ошибке.
Говард Хиннант
3
@Sascha: Во-первых, я определяю функцию в области пространства имен, потому что это единственный вид определения, который имеет значение для общего кода. Поскольку int et. и др. нет / не может иметь функций-членов, std :: sort et. и др. придется использовать бесплатную функцию; они устанавливают протокол. Во-вторых, я не знаю, почему вы возражаете против двух реализаций, но большинство классов обречены на неэффективную сортировку, если вы не можете принять своп, не являющийся членом. Правила перегрузки гарантируют, что если будут видны оба объявления, то при вызове swap без уточнения будет выбрано более конкретное (это).
Дэйв Абрахамс
5
@ Mozza314: Это зависит от обстоятельств. A, std::sortкоторый использует ADL для замены элементов, не соответствует C ++ 03, но соответствует C ++ 11. Кроме того, почему -1 ответ, основанный на том факте, что клиенты могут использовать неидиоматический код?
JoeG
4
@curiousguy: Если бы чтение стандарта было простым делом чтения стандарта, вы были бы правы :-). К сожалению, намерения авторов имеют значение. Так что, если исходное намерение состояло в том, что ADL можно или нужно использовать, оно не определено. Если нет, то это просто старое критическое изменение для C ++ 0x, поэтому я написал «в лучшем случае» недооцененным.
Dave Abrahams
70

Внимание Mozza314

Вот симуляция эффектов универсального 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имен «s, это просто легче запомнить (и учить других) , чтобы просто сделать это , что так.

Говард Хиннант
источник
4
Большое спасибо за подробный ответ. Я явно менее осведомлен об этом, и мне было интересно, как перегрузка и специализация могут привести к другому поведению. Однако я предлагаю не перегрузку, а специализацию. Когда я вставил template <>ваш первый пример, я получил вывод exp::swap(A, A)от gcc. Итак, почему бы не предпочесть специализацию?
voltrevo
1
Вот Это Да! Это действительно поучительно. Вы меня определенно убедили. Думаю, я немного изменю ваше предложение и воспользуюсь синтаксисом классного друга от Дэйва Абрахамса (эй, я могу использовать это и для оператора << тоже! :-)), если у вас нет причин избегать этого (кроме компиляции по отдельности). Кроме того, в свете этого, считаете ли вы, что using std::swapявляется исключением из правила «никогда не помещать операторы using в файлы заголовков»? На самом деле, почему бы не положить using std::swapвнутрь <algorithm>? Я полагаю, это могло бы сломать код крошечного меньшинства людей. Может быть, отказаться от поддержки и в конечном итоге добавить ее?
voltrevo
3
синтаксис друга в классе должен быть в порядке. Я бы попытался ограничить using std::swapобъем функций в ваших заголовках. Да, swapэто почти ключевое слово. Но нет, это не совсем ключевое слово. Поэтому лучше не экспортировать его во все пространства имен, пока это действительно не понадобится. swapочень нравится operator==. Самая большая разница в том, что никто даже не думает о вызове operator==с использованием квалифицированного синтаксиса пространства имен (это было бы слишком некрасиво).
Ховард Хиннант,
15
@NielKirk: То, что вы видите как осложнение, - это просто слишком много неправильных ответов. В правильном ответе Дэйва Абрахамса нет ничего сложного: «Правильный способ перегрузить своп - записать его в том же пространстве имен, что и то, что вы обмениваете, чтобы его можно было найти с помощью поиска, зависящего от аргументов (ADL)».
Ховард Хиннант,
2
@codeshot: Извините. Херб пытается донести это сообщение с 1998 года: gotw.ca/publications/mill02.htm Он не упоминает своп в этой статье. Но это всего лишь еще одно применение принципа интерфейса Херба.
Говард Хиннант,
53

Вам не разрешено (по стандарту 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 за указание на это)

Wilka
источник
1
В основном хороший совет, но мне нужно -1 из-за тонкого различия, отмеченного puetzk между специализацией шаблона в пространстве имен std (что разрешено стандартом C ++) и перегрузкой (что нет).
j_random_hacker
11
Проголосовали против, потому что правильный способ настроить своп - это сделать это в вашем собственном пространстве имен (как указывает Дэйв Абрахамс в другом ответе).
Ховард Хиннант,
2
Мои причины для голосования те же, что и у Говарда
Дэйв Абрахамс
13
@HowardHinnant, Дэйв Абрахамс: Я не согласен. На каком основании вы утверждаете, что ваша альтернатива является «правильной»? Как указано в стандарте puetzk, это специально разрешено. Хотя я новичок в этой проблеме, мне действительно не нравится метод, который вы отстаиваете, потому что, если я определю Foo и поменяю его таким образом, кто-то другой, использующий мой код, скорее всего, будет использовать std :: swap (a, b), а не swap ( а, б) на Foo, который молча использует неэффективную версию по умолчанию.
voltrevo
5
@ Mozza314: Ограничения по пространству и форматированию области комментариев не позволили мне полностью ответить вам. См. Добавленный мной ответ под названием «Внимание Mozza314».
Ховард Хиннант
29

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

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

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

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

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

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

puetzk
источник
7
Одна из причин, по которой неправильно использовать специализацию шаблона функции для этого (или чего-то еще): он плохо взаимодействует с перегрузками, из которых много для подкачки. Например, если вы специализируете обычный std :: swap для std :: vector <mytype> &, ваша специализация не будет выбрана по сравнению со стандартным зависящим от вектора свопом, потому что специализации не учитываются при разрешении перегрузки.
Дэйв Абрахамс,
4
Это также то, что Мейерс рекомендует в «Эффективном C ++ 3ed» (статья 25, стр. 106–112).
jww 05
2
@DaveAbrahams: Если вы специализируетесь (без аргументов явных шаблонов), частичное упорядочение приведет к его быть специализация в в vectorверсии и будет использоваться .
Дэвис Херринг
1
@DavisHerring на самом деле, нет, когда вы делаете это, частичное упорядочение не играет роли. Проблема не в том, что это вообще нельзя назвать; это то, что происходит при наличии явно менее специфичных перегрузок свопа: wandbox.org/permlink/nck8BkG0WPlRtavV
Дэйв Абрахамс
2
@DaveAbrahams: частичный порядок заключается в выборе шаблона функции для специализации, если явная специализация соответствует более чем одной. Добавленная ::swapвами перегрузка более специализирована, чем std::swapперегрузка vector, поэтому она перехватывает вызов, и никакая специализация последнего не имеет значения. Я не уверен, насколько это практическая проблема (но я и не утверждаю, что это хорошая идея!).
Дэвис Херринг