Функция обмена членами публичного друга

В прекрасном ответе copy-and-swap-idiom есть часть кода, мне нужно немного помочь:

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 static? Очевидно, что он не использует никаких переменных-членов.
  • "Любое правильное использование swap обнаружит swap через ADL" ? ADL будет искать пространства имен, не так ли? Но разве он также выглядит внутри классов? Или здесь, где friend входит?

Побочных вопросы:

  • С С++ 11, следует ли отмечать мой swap noexcept?
  • С С++ 11 и его range-for, должен ли я помещать friend iter begin() и friend iter end() таким же образом внутри класса? Я думаю, что friend здесь не нужен, правильно?

Ответ 1

Существует несколько способов написать 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? В этой области есть путаница.

До того, как С++ был стандартизирован, функции friend сделали что-то, называемое "инъекция имени друга", где код вел себя так, как если бы функция была написана в окружающем пространстве имен. Например, это были эквивалентные предварительные стандарты:

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

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

Однако, когда ADL был изобретен, это было удалено. Функция friend может быть найдена только через ADL; если вы хотите, чтобы это была бесплатная функция, ее нужно было объявить так (см. это, например). Но вот! Возникла проблема.

Если вы просто используете std::swap(x, y), ваша перегрузка никогда не будет найдена, потому что вы явно сказали "посмотрите в std, а нигде больше"! Вот почему некоторые люди предложили написать две функции: одну как функцию, которая будет найдена через ADL, а другая для обработки явных квалификаций std::.

Но, как мы видели, это не может работать во всех случаях, и мы заканчиваем уродливым беспорядком. Вместо этого, идиоматическая свопинг пошла по другому маршруту: вместо того, чтобы сделать работу классов для предоставления std::swap, это задача swappers, чтобы убедиться, что они не используют квалифицированный swap, как указано выше. И это работает очень хорошо, если люди об этом знают. Но в этом и заключается проблема: неинтересно использовать неквалифицированный вызов!

Чтобы сделать это проще, некоторые библиотеки, такие как Boost, предоставили функцию boost::swap, которая просто выполняет неквалифицированный вызов swap, с std::swap в качестве связанного пространства имен. Это помогает сделать вещи краткими снова, но это все еще облом.

Обратите внимание, что в С++ 11 нет изменений в поведении std::swap, которые, как мы ошибочно думали, я и другие. Если вам было немного, читайте здесь.


Короче: функция-член - это просто шум, специализация уродливая и неполная, но функция friend завершена и работает. И когда вы меняете, используйте boost::swap или неквалифицированный swap с std::swap.


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

Ответ 2

Этот код эквивалентен (почти всеми способами):

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 проекта С++ 0x):

Функция может быть определена в объявлении друга класса тогда и только тогда, когда класс является нелокальным классом (9.8), имя функции является неквалифицированным, а функция имеет область пространства имен.

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