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