функция обмена друзей

169

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

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

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

  • В C ++ 11 я должен пометить свой swaps noexcept?
  • С C ++ 11 и его диапазон-для , я должен поместить friend iter begin()и так friend iter end()же внутри класса? Я думаю, что friendздесь не нужно, верно?
towi
источник
Рассматривая дополнительный вопрос о диапазоне на основе для: лучше написать функции-члены и оставить доступ к диапазону для begin () и 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
}

Однако, когда ADL был изобретен, это было удалено. 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), что позволит его найти.

GManNickG
источник
10
Я не согласен с тем, что функция-член - это просто шум. Функция-член допускает, например std::vector<std::string>().swap(someVecWithData);, что невозможно со swapсвободной функцией, потому что оба аргумента передаются по неконстантной ссылке.
ildjarn
3
@ildjarn: Вы можете сделать это в две строки. Наличие функции-члена нарушает принцип СУХОГО.
GManNickG
4
@GMan: принцип СУХОЙ не применяется, если один реализован в терминах другого. Не иначе никто бы выступать класс с реализациями operator=, operator+и operator+=, но очевидно , эти операторы на соответствующих классах принимаются / , как ожидается, существует для симметрии. То же самое касается члена swap+ пространства имен swapпо моему мнению.
ildjarn
3
@GMan Я думаю, что это слишком много функций. Малоизвестно, но даже a function<void(A*)> f; if(!f) { }может потерпеть неудачу только потому, что Aобъявляет, operator!что принимает так fже хорошо, как fи свое operator!(маловероятно, но может случиться). Если function<>автор подумал «о, у меня есть« оператор bool », почему я должен реализовывать« оператор! »? Это нарушило бы СУХОЙ!», Это было бы фатальным. Вам просто нужно иметь operator!реализованный для Aи Aиметь конструктор для function<...>, и все пойдет не так, потому что оба кандидата потребуют определенных пользователем преобразований.
Йоханнес Шауб -
1
Давайте рассмотрим, как мы можем подумать о написании [swap] функции подкачки. Естественно, наш класс тоже должен, верно? Ну не совсем. В стандартной библиотеке есть все виды ненужных вещей , и обмен членами является одним из них. Связанный GotW выступает за функцию обмена членами.
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), имя функции является неквалифицированным, а функция имеет область пространства имен.

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

Бен Фойгт
источник
2
На самом деле, дружественные функции не помещаются во вложенное пространство имен в стандартном C ++. Старое поведение называлось «внедрение имени друга», но было заменено ADL, замененным в первом стандарте. Смотрите верхнюю часть этого . (Однако поведение очень похоже.)
GManNickG
1
Не совсем эквивалентно. Код в вопросе делает это так, что swapон виден только ADL. Он является членом окружающего пространства имен, но его имя не видно другим формам поиска имен. РЕДАКТИРОВАТЬ: я вижу, что @GMan снова был быстрее :) @Ben, это всегда было так в ISO C ++ :)
Йоханнес Шауб - Litb
2
@Ben: Нет, инъекция друга никогда не существовала в стандарте, но раньше она широко использовалась, поэтому идея (и поддержка компилятора) имела тенденцию продолжаться, но технически ее там нет. friendфункции находятся только в ADL, и если они должны быть просто свободными функциями с friendдоступом, они должны быть объявлены как friendвнутри класса, так и как обычное объявление свободной функции вне класса. Вы можете увидеть эту необходимость в этом ответе , например.
GManNickG
2
@towi: Поскольку функция друга находится в области имен, ответы на все три ваших вопроса должны стать ясными: (1) Это бесплатная функция, плюс у друга есть доступ к закрытым и защищенным членам класса. (2) Это вовсе не член, ни экземпляр, ни статический элемент. (3) ADL не выполняет поиск в классах, но это нормально, потому что у функции Friend есть область имен.
Бен Фойгт
1
@Бен. В спецификации функция является членом пространства имен, и фраза «функция имеет область действия пространства имен» может быть интерпретирована, чтобы сказать, что функция является членом пространства имен (это в значительной степени зависит от контекста такого оператора). И это добавляет имя к тому пространству имен, которое видимо только для ADL (фактически, какая-то часть IIRC противоречит другим частям в спецификации о том, добавляется ли какое-либо имя или нет. Но добавление имени необходимо для обнаружения несовместимых объявлений, добавленных к этому или нет). Пространство имен, так что на самом деле, невидимое название будет добавлено. См примечания в 3.3.1p4).
Йоханнес Шауб -