Почему двухэтапный поиск не может выбрать перегруженную версию "swap"?

Я изучаю этот увлекательный ответ на тонкий вопрос относительно лучшей практики для реализации swap для пользовательских типов. (Мой вопрос изначально был мотивирован обсуждением незаконности добавления типов в пространство имен std.)

Я не буду повторно распечатывать фрагмент кода из приведенного выше ответа.

Вместо этого я хотел бы понять ответ.

Ответ, который я связал выше, находится под первым фрагментом кода в отношении перегрузки swap в namespace std (вместо того, чтобы специализировать его в этом пространстве имен):

Если ваш компилятор печатает что-то другое, то это не правильно реализуя "двухфазный поиск" для шаблонов.

Далее следует отметить, что , специализирующийся на swap в namespace std (в отличие от перегрузки) создает другой результат (желаемый результат в случай специализации).

Однако в ответе идет дополнительный случай: специализированный swap для пользовательского класса шаблонов - в этом случае, опять же, желаемый результат не достигается.

К сожалению, ответ просто формулирует факты; он не объясняет почему.

Может кто-то прокомментировать этот ответ и описать процесс поиска в двух конкретных фрагментах кода, представленных в этом ответе:

  • перегрузка swap в namespace std для пользовательского класса без шаблона (как в первом фрагменте кода связанного ответа)

  • специализируется swap в namespace std для пользовательского класса шаблонов (как в последнем фрагменте кода связанного ответа)

В обоих случаях генерируется общий std::swap, а не пользовательский swap. Почему?

(Это прояснит характер двухфазного поиска и причину лучшей практики для реализации пользовательского swap; спасибо.)

Ответ 1

Преамбула с большим количеством стандартного

Вызов swap() в примере влечет за собой зависимое имя, потому что его аргументы begin[0] и begin[1] зависят от параметра шаблона T окружающего шаблона функции algorithm(). Поиск двухфазных имен для таких зависимых имен определяется в стандарте следующим образом:

14.6.4.2 Функции кандидата [temp.dep.candidate]

1 Для вызова функции, где постфиксное выражение является зависимым именем, функции кандидата найдены с использованием обычных правил поиска (3.4.1, 3.4.2), за исключением того, что:

- для части поиска, использующей поиск неквалифицированного имени (3.4.1), только объявления функций из определения шаблона контекст.

- для части поиска с использованием связанных пространства имен (3.4.2), только объявления функций, найденные либо в контекста определения шаблона или контекста создания шаблона найдено.

Неквалифицированный поиск определяется

3.4.1 Поиск неквалифицированного имени [basic.lookup.unqual]

1 Во всех случаях, перечисленных в 3.4.1, поиск областей осуществляется объявление в порядке, указанном в каждой из соответствующих категорий; поиск имени заканчивается, как только будет найдено объявление для имени. Если нет объявление найдено, программа плохо сформирована.

и зависящий от аргумента поиск (ADL) как

3.4.2 Поиск зависимых от аргументов имен [basic.lookup.argdep]

1 Когда постфиксное выражение в вызове функции (5.2.2) unqualified-id, другие пространства имен, не учтенные во время обычного неквалифицированный поиск (3.4.1), и в этих пространствах имен, функция имени пространства имен или назначение шаблона функции (11.3), которые не видны другим образом. Эти изменения в поиск зависит от типов аргументов (и шаблона шаблона аргументы, пространство имен аргумента шаблона).

Применение стандарта к примеру

первый пример вызывает exp::swap(). Это не зависимое имя и не требует поиска двухфазных имен. Поскольку вызов для свопинга является квалифицированным, происходит обычный поиск, который находит только общий шаблон функции swap(T&, T&).

второй пример (то, что @HowardHinnant называет "современным решением" ) вызывает swap(), а также имеет перегрузку swap(A&, A&) в том же пространстве имен, что и там, где class A живет (глобальное пространство имен в этом случае). Поскольку вызов для свопинга является неквалифицированным, как обычный поиск, так и ADL имеют место в точке определения (опять же только поиск общего swap(T&, T&)), но другой ADL имеет место в момент создания экземпляра (т.е. Когда exp::algorithm() вызывается в main()), и это поднимает swap(A&, A&), что является лучшим совпадением во время разрешения перегрузки.

Пока все хорошо. Теперь для бэка: третий пример вызывает swap() и имеет специализацию template<> swap(A&, A&) внутри namespace exp. Поиск аналогичен второму примеру, но теперь ADL не выбирает специализацию шаблона, потому что он не находится в ассоциированном пространстве имен class A. Однако, хотя специализация template<> swap(A&, A&) не играет роли во время разрешения перегрузки, она все же создается в месте использования.

Наконец, четвертый пример вызывает swap() и имеет перегрузку template<class T> swap(A<T>&, A<T>&) внутри namespace exp для template<class T> class A, живущих в глобальном пространстве имен. Поиск такой же, как в третьем примере, и снова ADL не поднимает перегрузку swap(A<T>&, A<T>&), потому что она не находится в ассоциированном пространстве имен шаблона класса A<T>. И в этом случае также нет специализации, которая должна быть создана в месте использования, поэтому здесь генерируется общий swap(T&, T&).

Заключение

Даже если вам не разрешено добавлять новые перегрузки в namespace std и только явные специализации, он даже не будет работать из-за различных сложностей поиска двухфазных имен.

Ответ 2

Невозможно перегрузить swap в namespace std для определенного пользователем типа. Введение перегрузки (в отличие от специализации) в namespace std - это поведение undefined (незаконное по стандарту, без необходимости диагностики).

Невозможно специализировать функцию вообще для класса template (в отличие от экземпляра класса template), т.е. std::vector<int> является экземпляром, тогда как std::vector<T> является целым классом template). То, что кажется специализацией, на самом деле является перегрузкой. Таким образом, применяется первый абзац.

Лучшей практикой реализации пользовательского swap является введение функции swap или перегрузки в том же пространстве имен, в котором живет ваш template или class.

Затем, если swap вызывается в правом контексте (using std::swap; swap(a,b);), то есть как он вызывается в библиотеке std, ADL будет входить, и ваша перегрузка будет найдена.

Другой вариант - выполнить полную специализацию swap в std для вашего конкретного типа. Это невозможно (или непрактично) для классов template, так как вам нужно специализироваться для каждого экземпляра вашего класса template, который существует. Для других классов он является хрупким, поскольку специализация относится только к определенному типу: подклассы должны быть также обработаны в std.

В целом специализация функций чрезвычайно хрупкая, и вам лучше вводить переопределения. Поскольку вы не можете вводить переопределения в std, единственное место, где они будут надежно найдены, находится в вашем собственном namespace. Такие переопределения в вашем собственном пространстве имен предпочтительнее над переопределениями в std.

Есть два способа вставить swap в ваше пространство имен. Оба работают для этой цели:

namespace test {
  struct A {};
  struct B {};
  void swap(A&, A&) { std::cout << "swap(A&,A&)\n"; }
  struct C {
    friend void swap(C&, C&) { std::cout << "swap(C&, C&)\n"; }
  };

  void bob() {
    using std::swap;
    test::A a, b;
    swap(a,b);
    test::B x, y;
    swap(x, y);
    C u, v;
    swap(u, v);
  }
}

void foo() {
  using std::swap;
  test::A a, b;
  swap(a,b);
  test::B x, y;
  swap(x, y);
  test::C u, v;
  swap(u, v);

  test::bob();
}
int main() {
  foo();
  return 0;
}

первый - это ввести его в namespace напрямую, второй - включить его как встроенный friend. Inline friend для "внешних операторов" - это общий шаблон, который в основном означает, что вы можете находить swap через ADL, но в этом конкретном контексте не много добавляется.