Як перевантажити std :: swap ()


115

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

Але реалізація std 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 для конкретного типу бетону, як показано в ОП; просто не сподівайтеся, що спеціалізація звикне, наприклад, для похідних класів цього типу.
Дейв Абрахамс

15
Я був би здивований, виявивши, що в реалізації все ще не використовується ADL, щоб знайти правильний своп. Це старе питання комітету. Якщо ваша реалізація не використовує ADL для пошуку swap, подайте звіт про помилку.
Говард Хінант

3
@Sascha: По-перше, я визначаю функцію в області простору імен, тому що це єдиний вид визначення, який має значення для загального коду. Тому що int et. ін. не / не можуть мати функції члена, std :: sort et. ін. доведеться використовувати вільну функцію; вони встановлюють протокол. По-друге, я не знаю, чому ви заперечуєте проти двох реалізацій, але більшість класів приречені на неефективне сортування, якщо ви не можете прийняти обмін не членами. Правила перевантаження гарантують, що якщо видно обидві декларації, то вибиратиметься більш конкретна (ця), коли своп викликається без кваліфікації.
Дейв Абрахамс

5
@ Mozza314: Це залежить. А, std::sortщо використовує ADL для заміни елементів, не відповідає C ++ 03, але відповідає C ++ 11. Крім того, чому -1 відповідь, заснована на тому, що клієнти можуть використовувати неідіоматичний код?
JoeG

4
@curiousguy: Якщо читання стандарту було просто простим питанням читання стандарту, ви будете праві :-). На жаль, наміри авторів мають значення. Отже, якщо початковий намір полягав у тому, що ADL може бути або повинен використовуватися, це не визначено. Якщо ні, то це просто звичайна стара переломна зміна для C ++ 0x, саме тому я написав "у кращому випадку" підкресленим.
Дейв Абрахамс

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простір імен, просто запам'ятати (а також навчити інших) просто робити це одним способом.


4
Дякую за детальну відповідь. Я, очевидно, менше знаю про це, і насправді цікавився, як перевантаження та спеціалізація можуть спричинити різну поведінку. Однак я не пропоную перевантаження, а спеціалізацію. Коли я додаю template <>ваш перший приклад, я отримую вихід exp::swap(A, A)від gcc. Отже, чому б не віддати перевагу спеціалізації?
voltrevo

1
Оце Так! Це справді освічуюче. Ви точно переконали мене. Я думаю, я трохи змінитиму вашу пропозицію та використаю синтаксис друга в класі від Дейва Абрахамса (ей, я можу використовувати це і для оператора <<! :-)), якщо у вас немає причин уникати цього (крім компіляції окремо). Також, зважаючи на це, чи вважаєте ви using std::swap, що виняток із правила "ніколи не використовуйте заяви у файлах заголовка"? Насправді, чому б не покласти using std::swapвсередину <algorithm>? Я гадаю, це може порушити крихітну меншину коду людей. Можливо, застаріла підтримка і врешті-решт її використати?
voltrevo

3
синтаксис друга в класі повинен бути добре. Я б спробував обмежитися using std::swapфункціонуванням у ваших заголовках. Так, swapце майже ключове слово. Але ні, це не зовсім ключове слово. Тож краще не експортувати його у всі простори імен, поки вам це не потрібно. swapдуже схожий operator==. Найбільша різниця полягає в тому, що ніхто ніколи навіть не думає дзвонити operator==з кваліфікованим синтаксисом простору імен (це було б занадто некрасиво).
Говард Хіннант

15
@NielKirk: Те, що ви сприймаєте як ускладнення, - це занадто багато неправильних відповідей. Немає нічого складного в правильній відповіді Дейва Абрахамса: "Правильний спосіб перевантажити своп - записати його в тому ж просторі імен, що і те, що ви обміняєте, щоб його можна було знайти за допомогою аргументованого пошуку (ADL)."
Говард Хіннант

2
@codeshot: Вибачте. Герб намагається отримати це повідомлення з 1998 року: gotw.ca/publications/mill02.htm У цій статті він не згадує своп. Але це лише чергове застосування принципу Herb's Interface.
Говард Хінант

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'oh! (дякую puetzk та j_random_hacker за вказівку на це)


1
Здебільшого хороша порада, але мені доводиться -1 через тонку різницю, яку відзначає puetzk, між спеціалізацією шаблону в просторі імен std (що дозволено стандартом C ++) та перевантаженням (що ні).
j_random_hacker

11
Тому що правильний спосіб налаштувати своп - це зробити у власному просторі імен (як вказує Дейв Абрахамс у іншій відповіді).
Говард Хіннант

2
Мої причини відхилення таких же, як і в Говарда
Дейв Абрахамс

13
@HowardHinnant, Дейв Абрахамс: Я не згоден. На якій підставі ви заявляєте, що ваша альтернатива є "правильним" способом? Як цитується puetzk із стандарту, це спеціально дозволено. Хоча я новачок у цьому питанні, мені дуже не подобається метод, який ви захищаєте, тому що якщо я визначу Foo і поміняю таким чином, хтось, хто використовує мій код, скоріше за все використовуватиме std :: swap (a, b), а не swap ( a, b) на Foo, який мовчки використовує неефективну версію за замовчуванням.
вольтрево

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) { ... }
}

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

Існує нитка на comp.lang.c ++. Модератор з довгим обговорення теми. Більшість це, однак, про часткову спеціалізацію (що в даний час немає хорошого способу зробити).


7
Однією з причин неправильного використання спеціалізованої шаблону функцій для цього (або будь-чого іншого): вона погано взаємодіє з перевантаженнями, яких для обміну існує багато. Наприклад, якщо ви спеціалізуєте звичайний std :: swap для std :: vector <mytype> &, ваша спеціалізація не буде обрана над стандартним свопом, характерним для вектора, оскільки спеціалізації не розглядаються під час вирішення перевантаження.
Дейв Абрахамс

4
Це також рекомендує Майєрс у Ефективній С ++ 3ed (Пункт 25, с. 106-112).
jww

2
@DaveAbrahams: Якщо ви спеціалізуєтесь (без аргументів явних шаблонів), часткове впорядкування призведе до його бути спеціалізація в в vectorверсії і буде використовуватися .
Девіс Хелінг

1
@DavisHerring насправді, ні, коли ви робите, що часткове замовлення не грає ніякої ролі. Проблема не в тому, що ви її взагалі не можете зателефонувати; це те, що відбувається за наявності, мабуть, менш специфічних перевантажень свопом: wandbox.org/permlink/nck8BkG0WPlRtavV
Дейв Абрахамс

2
@DaveAbrahams: Часткове впорядкування полягає у виборі шаблону функції для спеціалізації, коли явна спеціалізація відповідає більш ніж одному. ::swapВи додали перевантаження більш спеціалізовані , ніж std::swapперевантаження для vector, тому він захоплює виклик і ніякої спеціалізації останній є актуальною. Я не впевнений, наскільки це практична проблема (але я також не стверджую, що це гарна ідея!).
Девіс Хелінг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.