Как сделать так, чтобы ADL предпочитал шаблон функции другому
Мне было интересно, можно ли ADL выбрать шаблон функции, определенный в пространстве имен класса одного из аргументов (или в каком-то другом четко определенном месте) в ситуации, когда видны другие шаблоны функций. У меня есть мотивирующий пример, который следует, и хотя я знаю обходной путь для этого конкретного случая (я обсуждаю его ниже), вопрос в целом, кажется, имеет смысл.
Я подумал, что круто избегать использования объявлений друзей, а скорее делегировать работу методам, и поэтому придумал
namespace n
{
struct a
{
auto swap(a& a2) -> void;
};
auto swap(a& a1, a& a2) -> void
{
a1.swap(a2);
}
}
auto main(void) -> int
{
n::a a1, a2;
using std::swap;
swap(a1,a2); // use case 1
n::swap(a1,a2); // use case 2
}
Пока все хорошо, оба варианта использования работают нормально, но затем я добавил второй класс с его собственным методом подкачки и решил сэкономить на шаблоне, превратив автономный своп в шаблон:
namespace n
{
struct a
{
auto swap(a& a2) -> void;
};
struct b
{
auto swap(b& b2) -> void;
};
template<class T>
auto swap(T& t1, T& t2) -> void
{
t1.swap(t2);
}
}
auto main(void) -> int
{
n::a a1, a2;
using std::swap;
swap(a1,a2); // use case 1
n::swap(a1,a2); // use case 2
}
А вот в случае использования 1 ломается, компилятор жалуется на неоднозначность с std::swap
шаблон. Если кто-то предвидит проблему, можно определить swap
функции, которые являются более подходящими, чем методы (они обычно будут друзьями, так как они заменяют методы):
namespace n
{
struct a
{
friend auto swap(a& a1, a& a2) -> void;
};
struct b
{
friend auto swap(b& b1, b& b2) -> void;
};
}
Теперь все работает, так что в случае swap
Достаточно просто помнить, что использовать функции-друзья чаще, чем методы, но как насчет общего случая? Есть ли какой-нибудь хак, хоть и грязный, который позволил бы компилятору однозначно выбрать n::foo<a>
(или какой-то другой foo<a>
под нашим контролем) в ситуации, когда другие template<class T> foo
видны либо в глобальном пространстве имен, либо из-за некоторых using
пункт, особенно если последний не наш, чтобы изменить?
2 ответа
Виновник здесь не только в том, что ты пишешь using std::swap
, но принципиально, что вы предоставили свой собственный шаблон неограниченной функции swap
это даст ошибку разрешения перегрузки с std::swap
всякий раз, когда namespace std
рассматривается во время поиска имени (либо явным using
директива или по ADL).
Для иллюстрации: просто опуская using std::swap
спасет тебя в этом случае
auto main() -> int
{
n::a a1, a2;
swap(a1,a2); // use case 1
n::swap(a1,a2); // use case 2
}
Но предположим, что вы реорганизуете свои занятия a
а также b
в шаблоны классов b<T>
а также b<T>
и вызвать их с аргументом шаблона из namespace std
(например std::string
), то вы получите ошибку разрешения перегрузки:
#include <iostream>
#include <string>
namespace n
{
template<class>
struct a /* as before */;
template<class>
struct b /* as before */;
}
auto main() -> int
{
n::a<std::string> a1, a2; // oops, ADL will look into namespace std
swap(a1,a2); // use case 1 (ERROR)
n::swap(a1,a2); // use case 2 (OK)
}
Вывод: если вы определите свою собственную версию swap
с той же подписью, что и std::swap
(что касается разрешения перегрузки), всегда квалифицируйте вызовы к нему, чтобы отключить ADL.
Совет: еще лучше, не ленитесь, просто предоставьте свой swap
функция (не шаблон функции) для каждого класса в вашем собственном пространстве имен.
См. Также этот раздел вопросов и ответов, где объясняется аналогичный механизм, почему плохая идея предоставить свой собственный begin
а также end
шаблоны и ожидайте, что они будут работать с ADL.
Я знаю, что выгляжу глупо, чтобы отвечать на свой вопрос, но сам факт публикации и обсуждения действительно принес мне новое понимание.
В ретроспективе, что должно было поразить меня в первую очередь, это последовательность
using std::swap;
swap(a1,a2);
Это так по-старому, и это, очевидно, должно быть неправильно, поскольку для его многократного использования требуется копирование-вставка алгоритма (использования using
а потом обменяться). И не стоит копировать-вставлять, даже если алгоритм двухстрочный. Так что можно сделать лучше об этом? Как насчет того, чтобы превратить его в однострочник?
stdfallback::do_swap(a1,a2);
Позвольте мне предоставить код, который позволяет это:
namespace stdfallback
{
template<class T>
auto lvalue(void) -> typename std::add_lvalue_reference<T>::type;
template <typename T>
struct has_custom_swap
{
template<class Tp>
using swap_res = decltype(swap(lvalue<Tp>(),lvalue<Tp>()));
template <typename Tp>
static std::true_type test(swap_res<Tp> *);
template <typename Tp>
static std::false_type test(...);
static const bool value = decltype(test<T>(nullptr))::value;
};
template<class T>
auto do_swap(T& t1, T& t2) -> typename std::enable_if<has_custom_swap<T>::value,void>::type
{
swap(t1,t2);
}
template<class T>
auto do_swap(T& t1, T& t2) -> typename std::enable_if<!has_custom_swap<T>::value,void>::type
{
std::swap(t1,t2);
}
}
В решении вы найдете класс черт на основе SFINAE has_custom_swap
чья value
Значение true или false, в зависимости от того, найден ли неквалифицированный вызов подкачки для значений l типа экземпляра (для этого нужно lvalue
шаблон, похожий на declval
но преобразование в l-значение, а не в r-значение), а затем две перегрузки do_swap
метод для случая, когда пользовательский своп присутствует, а когда его нет. Они должны называться иначе, чем swap
в противном случае тот, который вызывает неквалифицированный пользовательский своп, не компилируется, потому что он сам по себе неоднозначен с тем свопом, который пытается вызвать.
Так что, возможно, мы должны рассмотреть возможность использования этого шаблона вместо установленного using
?
(Чтобы дать должное признание, решение черт было вдохновлено http://blog.quasardb.net/sfinae-hell-detecting-template-methods/)