функция-член обмена общими друзьями

В красивом ответе на идиому копирования и обмена есть фрагмент кода, мне нужна небольшая помощь:

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 для нашего типа, предоставлять внутриклассный своп наряду с подкачкой свободных функций и т. Д. Но это все ненужно: любое правильное использование подкачки будет через неквалифицированный вызов , а нашу функцию можно будет найти через ADL. Подойдет одна функция.

Я должен признать, что с friend я немного "недружелюбен". Итак, мои основные вопросы:

  • выглядит как бесплатная функция, но находится ли она внутри тела класса?
  • почему это не swap статично? Очевидно, что он не использует никаких переменных-членов.
  • «Любое правильное использование свопа обнаружит своп через ADL»? ADL будет искать в пространствах имен, верно? Но заглядывает ли он также внутрь классов? Или здесь появляется friend?

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

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

person towi    schedule 17.04.2011    source источник
comment
Учитывая побочный вопрос о диапазоне на основе для: лучше написать функции-члены и оставить доступ к диапазону в begin () и end () в пространстве имен std (§24.6.5), на основе диапазона для внутреннего использования их из global или std (см. §6.5.4). Однако есть и обратная сторона: эти функции являются частью заголовка ‹iterator›, и если вы его не включите, вы можете написать их самостоятельно.   -  person Vitus    schedule 18.04.2011
comment
почему это не статично - потому что функция friend вообще не является функцией-членом.   -  person aschepler    schedule 09.08.2016


Ответы (2)


Есть несколько способов написать 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), что позволит его найти.

person GManNickG    schedule 17.04.2011
comment
Главный ответ снова. :) Может быть, добавить именно то, что вы имеете в виду, с with std :: swap` associated`? - person Xeo; 17.04.2011
comment
Всего один простой запрос: можете ли вы предоставить ссылку на часть черновика C ++ 11, в которой говорится, что std :: swap сначала должен использовать подкачку, найденную в ADL? Кажется, я не могу его найти (только соответствующая часть - это §17.6.3.2, но это касается только стандартной библиотеки). - person Vitus; 18.04.2011
comment
@GMan: Да, я знаю об этом. Но, учитывая, что std :: swap в ‹utility› (§20.2.2) не упоминает концепцию Swappable, не похоже, что какое-либо общее использование std :: swap гарантированно использует подходящий подкачку, найденную в ADL. - person Vitus; 18.04.2011
comment
Я не согласен с тем, что функция-член - это просто шум. Функция-член позволяет, например, std::vector<std::string>().swap(someVecWithData);, что невозможно с swap свободной функцией, потому что оба аргумента передаются по неконстантной ссылке. - person ildjarn; 18.04.2011
comment
@ildjarn: вы можете сделать это двумя строками. Наличие функции-члена нарушает принцип DRY. - person GManNickG; 18.04.2011
comment
@GMan: Принцип DRY неприменим, если один реализован в терминах другого. В противном случае никто не стал бы защищать класс с реализациями operator=, operator+ и operator+=, но очевидно, что эти операторы в соответствующих классах принимаются / должны существовать для симметрии. На мой взгляд, то же самое и с участником swap + с ограниченным пространством имен swap. - person ildjarn; 18.04.2011
comment
@GMan: И было бы 3 или 4 строки против 1, чтобы получить те же скобки для подсчета объема, а не 2 против 1.; -] - person ildjarn; 18.04.2011
comment
@ildjarn: operator+ и operator+= - операторы языка; то есть, если + работает, я ожидаю, что += тоже. Напротив, у std::swap нет партнеров. И количество строк не имеет значения, все равно как часто вы это делаете? Оберните это функцией, если она вас беспокоит. - person GManNickG; 18.04.2011
comment
Когда-то я был большим фанатом ADL. Но постепенно я все больше ненавижу это. - person Johannes Schaub - litb; 18.04.2011
comment
@Johannes: Почему? Все нюансы в этом? D избавился от этого, отбросив пространства имен. - person GManNickG; 18.04.2011
comment
@GMan Я думаю, это слишком много функций. Малоизвестно, но даже function<void(A*)> f; if(!f) { } может потерпеть неудачу только потому, что A объявляет operator!, который принимает f так же хорошо, как и собственный operator! f (маловероятно, но может случиться). Если автор function<> подумал, ох, у меня есть 'operator bool', зачем мне реализовывать 'operator!'? Это нарушит DRY !, и это будет фатальным. Вам просто нужно иметь operator!, реализованный для A, и A, имеющий конструктор для function<...>, и все сломается, потому что оба кандидата потребуют определенных пользователем преобразований. - person Johannes Schaub - litb; 18.04.2011
comment
@Johannes: Вау, это довольно дерьмо. : / - person GManNickG; 18.04.2011
comment
Это ИМО - серьезное препятствие для общих функций, которые хотят работать даже с пакетами параметров, типы которых могут содержать произвольные классы, каждый из которых имеет свои собственные операторы. Проблемы также возникают из-за того, что ADL потребует, чтобы A был создан экземпляр, если это специализация шаблона класса, что иногда приводит к неожиданным сбоям. См. llvm.org/bugs/show_bug.cgi?id=9440 и следующие комментарии / связанный GCC PR. - person Johannes Schaub - litb; 18.04.2011
comment
@Johannes: Это интересно. Думаете, его когда-нибудь доработают, чтобы в нем было меньше функций? - person GManNickG; 18.04.2011
comment
@GMan: Суть в том, что swap и член swap с областью имен могут предлагать разную семантику, поэтому по определению наличие обоих не является нарушением DRY, особенно с учетом того, что один из них реализован с точки зрения разное. Если бы семантика всегда была одинаковой, я бы с вами согласился. - person ildjarn; 18.04.2011
comment
@ildjarn: Тогда я откажусь от своего СУХОГО комментария, но это все равно шум. Это редко используемая функция, которая имитирует существующую функцию. Если замена временного - ваш единственный пример, это вообще не аргумент, просто пропустите временное в его собственной строке или напишите служебную функцию. - person GManNickG; 22.04.2011
comment
@GManNickG Поскольку C ++ 0x теперь стал C ++ 11, это К счастью, C ++ 0x это исправляет. Было предписано, чтобы std :: swap сначала выбирал перегрузки (в том числе найденные через ADL)! все еще верно? У меня было обсуждение с кем-то на cpp11.generisch.de/swap-operator, где мы были не уверен, что std::swap(a,b); теперь можно использовать безопасно / идиоматически. - person towi; 09.10.2013
comment
@towi: К сожалению, нет. Он сохраняет свое старое поведение, изменение было лакомым кусочком надежды и дезинформации, распространившейся по StackOverflow (где это началось, я не знаю). Я спросил, когда появился C ++ 11: stackoverflow.com/q/9170247/87234. Спасибо, что привлекли мое внимание к этому экземпляру. - person GManNickG; 09.10.2013
comment
@ JohannesSchaub-litb, но никто бы не оказался в такой ситуации из-за того, что всегда старался применять безопасную идиому bool, нет? - person v.oddou; 03.04.2015
comment
GotW, о котором вы упоминаете в своем ответе (gotw.ca/gotw/084.htm) перечисляет участника swap по мере необходимости, поэтому вам не следует цитировать эту статью в качестве доказательства своей противоположной идеи. - person Adam Badura; 13.04.2015
comment
@GManNickG что означает UB? важный! специализация в std в порядке, перегрузка - UB - person athos; 19.09.2016
comment
@athos: неопределенное поведение. - person GManNickG; 19.09.2016
comment
Давайте рассмотрим, что мы можем подумать о написании функции обмена [member]. Естественно, тогда и наш класс должен, верно? Ну не совсем. В стандартной библиотеке есть всякие ненужные вещи, и обмен участниками является одним из них. . Связанный GotW поддерживает функцию обмена членами. - person Xeverous; 03.12.2018
comment
Наконец, стандартизован в C ++ 20, см. this. - person Wormer; 15.08.2019
comment
Начиная с C ++ 20, UB также специализируется на стандартных шаблонах функций. И std::swap даже не будет фактическим шаблоном функции. - person L. F.; 08.09.2019

Этот код эквивалентен (почти во всех отношениях):

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), имя функции неквалифицировано и функция имеет область пространства имен.

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

person Ben Voigt    schedule 17.04.2011
comment
Фактически, в стандартном C ++ дружественные функции не помещаются во внутреннее пространство имен. Старое поведение называлось инъекцией имени друга, но было заменено ADL, замененным в первом стандарте. См. Начало этого. (Хотя поведение очень похоже.) - person GManNickG; 17.04.2011
comment
@GMan: Изменилось ли это между C ++ 03 и C ++ 0x? - person Ben Voigt; 17.04.2011
comment
встроенный ясен. Хорошо, есть еще один ответ, который мне поможет. - person towi; 17.04.2011
comment
Не совсем то же самое. Код в вопросе делает так, что swap виден только ADL. Он является членом окружающего пространства имен, но его имя не видно другим формам поиска имени. РЕДАКТИРОВАТЬ: я вижу, что @GMan снова был быстрее :) @Ben всегда так было в ISO C ++ :) - person Johannes Schaub - litb; 17.04.2011
comment
@Ben: Нет, инъекция друга никогда не существовала в стандарте, но она широко использовалась раньше, поэтому идея (и поддержка компилятора) имела тенденцию сохраняться, но технически ее там нет. friend функции могут быть найдены только ADL, и если они должны быть просто свободными функциями с friend доступом, они должны быть объявлены как friend внутри класса и как обычное объявление свободной функции вне класса. Вы можете увидеть эту необходимость, например, в этом ответе. - person GManNickG; 17.04.2011
comment
Я думаю, что добираюсь туда. но ... @JohannesSchaub: другие формы поиска имени? Что невозможно таким образом? У вас есть пример? - person towi; 17.04.2011
comment
@towi: Поскольку функция друга находится в области пространства имен, ответы на все три ваших вопроса должны стать ясными: (1) Это бесплатная функция, плюс есть доступ друзей к закрытым и защищенным членам класса. (2) Это вообще не член, ни экземпляр, ни статика. (3) ADL не выполняет поиск внутри классов, но это нормально, потому что функция друга имеет область пространства имен. - person Ben Voigt; 17.04.2011
comment
@GMan, @Johannes: В черновике C ++ 0x говорится, что функция друга имеет область пространства имен. Представляет ли это изменение по сравнению с C ++ 03, или он все еще не является членом пространства имен (и, следовательно, не может быть квалифицирован пространством имен)? - person Ben Voigt; 17.04.2011
comment
@Бен. В спецификации функция является членом пространства имен, а фразу, которую функция имеет область пространства имен, можно интерпретировать как указание на то, что функция является членом пространства имен (это в значительной степени зависит от контекста такого оператора). И он добавляет имя в это пространство имен, которое видно только ADL (на самом деле, некоторые части IIRC противоречат другим частям в спецификации о том, добавлено ли какое-либо имя или нет. Но добавление имени необходимо для обнаружения несовместимых объявлений, добавленных к этому namespace, поэтому фактически добавляется невидимое имя . См. примечание в 3.3.1p4). - person Johannes Schaub - litb; 17.04.2011
comment
@Johannes: Не то, как я это понимаю. [basic.scope.hiding] 3.3.10p5 говорит, что если имя находится в области видимости и не скрыто, оно считается видимым. [class.friend] означает, что функция имеет область пространства имен. Ни один из 3.3.10p1-4 не указывает, что функция скрыта, поэтому она видима. - person Ben Voigt; 17.04.2011
comment
@Ben не говорит, что функция name имеет область пространства имен. В нем говорится, что сама функция должна иметь область пространства имен. Термин «область применения» и связанные с ним термины перегружены и даже определены непоследовательно. См. open-std.org/jtc1/sc22/wg21 /docs/cwg_active.html#554. Таким образом, всякий раз, когда используется термин «область видимости», его реальное значение зависит от контекста его использования, а не только от того, что в 3.3 говорится о его определении. В этом контексте в других параграфах говорится, что имя друга не может быть найдено обычным поиском имени, если оно явно не объявлено в пространстве имен. - person Johannes Schaub - litb; 17.04.2011
comment
Я считаю, что фраза может быть определена, если ... функция имеет область пространства имен. просто запрещает struct A { void f(); struct B { friend void f() { } }; };. Некоторые реализации принимают это, но формулировка стандарта гласит, что следует искать f, а области за пределами самого внутреннего включающего пространства имен должны игнорироваться. область действия A находится внутри самого внутреннего включающего пространства имен, поэтому ее область действия не следует игнорировать. f объявлен там, поэтому friend void f() { } попытается определить член, не являющийся членом пространства имен, что, как мне кажется, этот абзац намеревается запретить. - person Johannes Schaub - litb; 17.04.2011
comment
Но, возможно, фразу можно определить, если ... функция имеет область пространства имен. был добавлен только для избыточности, чтобы было особенно ясно, что такие вещи могут использоваться только для определения членов пространства имен, без каких-либо других намерений. Но я не думаю, что это говорит о том, что имя функции видно во внутреннем пространстве имен. Выражение, начинающееся с, может быть определено, если .... - person Johannes Schaub - litb; 17.04.2011
comment
@Johannes: Я согласен с тем, что стандарт запрещает это. Но разве не кажется, что объявление friend, которое одновременно является определением и также вводит имя, должно помещать это имя в пространство имен, иначе это правило также применимо и запретит его? - person Ben Voigt; 17.04.2011