функція члена публічного обміну товаришами


169

У гарній відповіді на ідіому copy-and-swap є фрагмент коду, мені потрібна трохи допомога:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        using std::swap; 
        swap(first.mSize, second.mSize); 
        swap(first.mArray, second.mArray);
    }
    // ...
};

і він додає записку

Є й інші твердження, що ми повинні спеціалізуватися на std :: swap для нашого типу, забезпечити своп класу разом зі свопом вільної функції тощо. Але це все зайве: будь-яке правильне використання swap буде здійснюватися через некваліфікований дзвінок , і наша функція буде знайдена через ADL. Одну функцію буде виконувати.

Зі friendмною трохи на "недружніх" умовах, мушу визнати. Отже, мої основні питання:

  • виглядає як вільна функція , але всередині корпусу класу?
  • чому це не swapстатично ? Очевидно, він не використовує жодних змінних членів.
  • "Будь-яке правильне використання swap виявить своп через ADL" ? ADL здійснить пошук у просторах імен, правда? Але чи це також виглядає всередині класів? Або тут, де friendзаходить?

Побічні запитання:

  • З C ++ 11, чи слід позначити свій swaps noexcept?
  • З C ++ 11 і його діапазон через , я повинен поставити friend iter begin()і friend iter end()так само , як всередині класу? Я думаю, що friendтут не потрібно, правда?

Розглядаючи побічне питання щодо діапазону на основі: краще ідею написати функції учасників і залишити доступ до діапазону на start () та end () у просторі імен std (§ 24.6.5), на основі діапазону для внутрішнього використання їх із глобального або std простір імен (див. §6.5.4). Однак, із недоліком ці функції є частиною заголовка <iterator>, якщо ви не включите його, можливо, ви захочете записати їх самостійно.
Вітус

2
чому це не статично - адже friendфункція взагалі не є членом.
aschepler

Відповіді:


175

Існує кілька способів написання swap, деякі кращі за інші. Однак з часом було виявлено, що одне визначення найкраще працює. Розглянемо, як ми могли б подумати над написанням swapфункції.


Спочатку ми бачимо, що такі контейнери std::vector<>мають функцію члена з одним аргументом swap, наприклад:

struct vector
{
    void swap(vector&) { /* swap members */ }
};

Природно, тоді і наш клас повинен бути, правда? Ну не дуже. У стандартній бібліотеці є всілякі непотрібні речі , а член swap- один із них. Чому? Давайте продовжимо.


Що ми повинні зробити, це визначити, що є канонічним та що наше заняття повинно зробити для роботи з ним. І канонічний метод заміни - с std::swap. Ось чому функції членів не корисні: вони взагалі не так, як ми повинні міняти речі, і не мають жодного стосунку до поведінки std::swap.

Ну тоді, щоб зробити std::swapроботу, ми повинні забезпечити (і std::vector<>повинні були забезпечити) спеціалізацію std::swap, правда?

namespace std
{
    template <> // important! specialization in std is OK, overloading is UB
    void swap(myclass&, myclass&)
    {
        // swap
    }
}

Добре, що, безумовно, спрацює в цьому випадку, але в ньому є яскрава проблема: спеціалізація функцій не може бути частковою. Тобто ми не можемо спеціалізувати шаблонні класи з цим, лише окремими моментами:

namespace std
{
    template <typename T>
    void swap<T>(myclass<T>&, myclass<T>&) // error! no partial specialization
    {
        // swap
    }
}

Цей метод працює деякий час, але не весь час. Має бути кращий спосіб.


Є! Ми можемо використовувати friendфункцію та знаходити її через ADL :

namespace xyz
{
    struct myclass
    {
        friend void swap(myclass&, myclass&);
    };
}

Коли ми хочемо щось поміняти, ми пов’язуємо †, std::swap а потім робимо некваліфікований дзвінок:

using std::swap; // allow use of std::swap...
swap(x, y); // ...but select overloads, first

// that is, if swap(x, y) finds a better match, via ADL, it
// will use that instead; otherwise it falls back to std::swap

Що таке friendфункція? Навколо цього району існує плутанина.

Перш ніж стандартизувати C ++, friendфункції робили щось, що називалося "введення імені друга", де код поводився так, ніби функція була записана у навколишньому просторі імен. Наприклад, це були еквівалентні попередні стандарти:

struct foo
{
    friend void bar()
    {
        // baz
    }
};

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

Однак коли було винайдено АДЛ, це було видалено. Потім friendфункцію можна було знайти лише через ADL; якщо ви хотіли це як вільну функцію, її потрібно було оголосити так ( див. це , наприклад). Але ось! Виникла проблема.

Якщо ви просто використовуєте std::swap(x, y), ваше перевантаження ніколи не буде знайдено, тому що ви прямо сказали «загляньте std, і більше нікуди»! Ось чому деякі люди запропонували написати дві функції: одну як функцію, яку можна знайти через ADL , а другу для явної std::кваліфікації.

Але, як ми бачили, це не може працювати у всіх випадках, і ми закінчуємо неприємний безлад. Натомість ідіоматичне обмінювання пішло іншим маршрутом: замість того, щоб зробити його завданням класів забезпечити std::swap, це робота свопперів, щоб переконатися, що вони не використовують кваліфікованих swap, як вище. І це, як правило, працює досить добре, доки люди знають про це. Але в цьому криється проблема: неінтуїтивно потрібно використовувати некваліфікований дзвінок!

Щоб полегшити це, деякі бібліотеки на зразок Boost надали функцію boost::swap, яка просто виконує некваліфікований дзвінок swap, std::swapяк асоційований простір імен. Це допомагає знову зробити лаконічні речі, але це все-таки облом.

Зауважте, що C ++ 11 у поведінці немає змін std::swap, про що я та інші помилково вважали, що це буде так. Якщо вас це трохи покусало, читайте тут .


Коротше кажучи: функція члена - це просто шум, спеціалізація - потворна і неповна, але friendфункція завершена і працює. І коли ви обмінюєтесь, використовуйте boost::swapабо некваліфікуйте swapз std::swapасоційованим.


† Неофіційно ім'я асоціюється, якщо воно буде враховано під час виклику функції. Детальніше читайте §3.4.2. У цьому випадку std::swapзазвичай це не враховується; але ми можемо пов’язати його (додати його до набору перевантажень, розглянутих некваліфікованими swap), що дозволить його знайти.


10
Я не згоден, що функція члена - це лише шум. Функція-член допускає, наприклад std::vector<std::string>().swap(someVecWithData);, що неможливо з swapвільною функцією, тому що обидва аргументи передаються без посилання на const.
ildjarn

3
@ildjarn: Ви можете це зробити у двох рядках. Наявність функції члена порушує принцип DRY.
GManNickG

4
@GMan: Принцип DRY не застосовується, якщо один реалізований у терміні іншого. Не інакше ніхто б виступати клас з реалізаціями operator=, operator+і operator+=, але очевидно , ці оператори на відповідних класах приймаються / , як очікується, існує для симетрії. Те саме стосується і учасника swap+ простір імен, swapна мій погляд.
ildjarn

3
@GMan Я думаю, що це розглядає занадто багато функцій. Маловідоме, але навіть function<void(A*)> f; if(!f) { }може бути невдалим лише тому, що Aзаявляє, operator!що сприймає так fсамо добре, як fі своє operator!(навряд чи, але може статися). Якщо function<>автор подумав: "Ой, у мене є" операторський бул ", навіщо мені реалізовувати" оператора! "? Це порушило б DRY!", Це було б фатально. Вам просто потрібно мати operator!реалізований A, і Aмає конструктора для function<...>, і речі будуть ламатися, тому що обидва кандидати будуть вимагати певні користувачем перетворення.
Йоханнес Шауб - ліб

1
Розглянемо, як ми могли б подумати над написанням функції [member] swap. Природно, тоді і наш клас повинен бути, правда? Ну не дуже. У стандартній бібліотеці є всілякі непотрібні речі , а підміна члена - одна з них. Пов'язаний GotW виступає за функцію swap членів.
Xeverous

7

Цей код еквівалентний ( майже всіма способами):

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second);
    // ...
};

inline void swap(dumb_array& first, dumb_array& second) // nothrow
{
    using std::swap; 
    swap(first.mSize, second.mSize); 
    swap(first.mArray, second.mArray);
}

Друга функція, визначена в класі:

  • розміщений у приміщенні простору імен
  • автоматично inline
  • вміє посилатися на статичних членів класу без додаткової кваліфікації

Точні правила містяться в розділі [class.friend](я цитую пункти 6 та 7 проекту C ++ 0x):

Функція може бути визначена в другій декларації класу, якщо і лише якщо клас - це не локальний клас (9.8), ім'я функції не кваліфіковане, а функція має область простору імен.

Така функція неявно вбудована. Функція friend, визначена в класі, знаходиться в (лексичній) області класу, в якому вона визначена. Функція друга, визначена поза класом, не є.


2
Насправді функції друзів не розміщуються у вкладеному просторі імен, у стандартному C ++. Стару поведінку називали "ін'єкцією імені друга", але її витіснив ADL, замінений у першому стандарті. Дивіться верхню частину цього . (Однак поведінка досить схожа.)
GManNickG

1
Не дуже рівнозначно. Код у питанні робить його таким, що swapвидно лише ADL. Це член простору імен, що додається, але його ім’я не видно в інших формах пошуку імен. EDIT: Я бачу, що @GMan знову був швидшим :) @Ben це завжди було так у ISO C ++ :)
Йоханнес Шауб - ліб

2
@Ben: Ні, ін'єкція друзів ніколи не існувала в стандарті, але вона широко застосовувалася до цього, тому ідея (і підтримка компілятора), як правило, продовжувалася, але технічно її немає. friendфункції знаходять лише ADL, і якщо їм потрібно просто бути вільними функціями з friendдоступом, вони повинні бути оголошені як friendу межах класу, так і як звичайне вільне оголошення функції поза класом. Ви можете бачити цю необхідність, наприклад, у цій відповіді .
GManNickG

2
@towi: Оскільки функція friend знаходиться в межах простору імен, відповіді на всі три ваші запитання повинні стати зрозумілими: (1) Це безкоштовна функція, плюс доступ до друзів до приватних та захищених членів класу. (2) Це зовсім не член, ні екземпляр, ні статичний. (3) ADL не здійснює пошук у класах, але це нормально, оскільки функція friend має область простору імен.
Ben Voigt

1
@Ben. У специфікації функція є членом простору імен, і словосполучення "функція має область простору імен" можна інтерпретувати так, щоб сказати, що функція є членом простору імен (це значною мірою залежить від контексту такого висловлювання). І це додає ім'я до того простору імен, яке видно лише ADL (фактично, якась частина IIRC суперечить іншим частинам у специфікації щодо того, додано чи ні якесь ім'я. Але додавання імені потрібно для виявлення несумісних декларацій, доданих до цього простір імен, так що насправді, невидиме назва буде додано. Див примітки в 3.3.1p4).
Йоханнес Шауб - ліб
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.