Отказ от ответственности: я знаю, что использование пользовательских неявных преобразований часто не рекомендуется. Однако в нашем проекте нам нужны эти преобразования, чтобы различные классы шаблонов хорошо работали друг с другом.
Мне нужно определить приоритет пользовательских преобразований, например:
struct X{}
struct Y{}
struct Z{
operator X(){...}
operator Y(){...}
}
void foo(X x){...}
void foo(Y y){...}
// somewhere in some template client code
...{
Z z = ...;
...
foo(z); // WILL NOT COMPILE
}
Это не скомпилируется, так как преобразование из Z
либо X
или же Y
неоднозначно. Есть ли способ решить эту двусмысленность. Т.е. можно ли как-то сказать компилятору: есть ли перегруженная функция для X
а также Y
тогда предпочитаешь кастовать Z
в X
вместо того, чтобы не скомпилировать.
Я знаю, что не существует простого способа указать это. Но, возможно, некоторая магия шаблонов и структура шаблона упаковки, о которой я не знаю, могут сработать. Мне разрешено изменять код клиента. Я не могу использовать явное приведение, поскольку клиентский код представляет собой шаблонный код, который не знает о типе Z
и доступные перегрузки foo
,
Вы можете сделать что-то вроде «приоритетный вызов», с шаблоном, подобным этому:
struct P2 {};
struct P1: P2 {};
template<class A>
void foo(A x, P1, typename std::common_type<X,A>::type* =nullptr)
{ foo(static_cast<X>(x)); }
template<class A>
void foo(A y, P2, typename std::common_type<Y,A>::type* =nullptr)
{ foo(static_cast<Y>(y)); }
template<class A> void foo(A a) { foo(a,P1()); }
Будучи P2 базой для P1 и вызовом, сделанным с P1, если common_type может скомпилироваться, первая версия идет. Если он не может скомпилироваться, первая версия как бы не существует (SFINAE), а вторая выходит. Если он также не может скомпилировать … если A — это просто X или просто Y, то вызывается соответствующий исходный foo, иначе это не может скомпилировать несовместимые типы.
Обратите внимание, что вы можете даже обобщить «приоритет» как
template<size_t N> struct P: P<N+1> {}
template<> struct P<10> {}
объявлять функции SFINAE P<1>
, P<2>
и т. д. до P<10>
и выполните корневой вызов с помощью P<0>()
Вот небольшая система, которая умно приводит переменную к последовательности типов Ts...
такой, что первый элемент в списке Ts...
что переменная неявно преобразуется в выбранную:
namespace details {
template<class...>struct types{using type=types;};
template<class U, class Types, class=void>
struct smart_cast_t:std::false_type {
using type=U;
template<class A>
U operator()(A&& a)const{return std::forward<A>(a);}
};
template<class U, class T0, class...Ts>
struct smart_cast_t<
U, types<T0, Ts...>,
typename std::enable_if<std::is_convertible<U, T0>::value>::type
>:std::true_type
{
using type=T0;
template<class A>
T0 operator()(A&& a)const{return std::forward<A>(a);}
};
template<class U, class T0, class...Ts>
struct smart_cast_t<
U, types<T0, Ts...>,
typename std::enable_if<!std::is_convertible<U, T0>::value>::type
>:smart_cast_t< U, types<Ts...> >
{};
}
template<class... Ts, class U>
auto smart_cast( U&& u )
-> decltype(details::smart_cast_t< U, details::types<Ts...> >{}( std::forward<U>(u) ))
{
return details::smart_cast_t< U, details::types<Ts...> >{}( std::forward<U>(u) );
}
Теперь идея заключается в том, что теперь мы можем изменить foo
следующее:
void foo_impl(X);
void foo_impl(Y);
template<class A>
void foo(A&& a) {
foo_impl( smart_cast<X, Y>(std::forward<A>(a)) );
}
а также foo
бросает A
в X
если возможно, а если нет Y
,
Мы могли бы написать всю систему, в которой мы передаем описание перегрузок foo
в упаковке, как types< types<X,Y> >
и набор перегрузки foo
к некоторому магическому коду, который выплевывает диспетчер, но это было бы по инженерному делу.
Это использует функции C ++ 11. С C ++ 14 мы можем отбросить некоторые ошибки smart_cast
,
Дизайн довольно прост. Мы создаем types
связка для работы со связками типов (просто шаблон).
Тогда наш details::smart_cast_t
имеет запасную базовую специализацию, которая просто преобразует наши U
в U
, Если мы можем преобразовать U
к первому типу мы так и делаем. В противном случае, мы возвращаемся к нашему родительскому типу, возможно, заканчивая базовой специализацией.
Я скрываю это в деталях, поэтому наша публичная функция проста — это smart_cast< type1, type2, type3, etc >( expression )
, Нам не нужно проходить U
тип выражения, как мы его выводим, а затем передать его в детали для работы, которая будет сделана.
Во время выполнения это сводится к одному неявному приведению: все вышеперечисленное делается во время компиляции.
Единственным недостатком является то, что это может привести к предупреждению на некоторых компиляторах, так как мы используем неявное преобразование. Добавить static_cast<T0>
к первой неосновной специализации smart_cast_t
чтобы избежать этого.
Я унаследовал smart_cast_t
от true_type
а также false_type
без необходимости. Это означает, что значение smart_cast_t<U, types<Ts...>>::value
скажет вам, если U
преобразован в любой из Ts...
или просто остался один как U
,
Вызывающий отвечает за категории rvalue vs lvalue. Если приведение не выполнено, возвращается U
не U&&
если прошло rvalue. Запасной вариант — к U
— Я думаю, что будет генерировать лучшие сообщения об ошибках, но если вы предпочитаете это smart_cast<int, double>(std::string("hello"))
не скомпилировать вместо того, чтобы возвращать std::string
Просто удалите operator()
от базовой специализации smart_cast_t
,
Кстати говоря, я тоже не работаю smart_cast<int, double>("")
, Может понадобиться немного typename std::decay<U>::type
с или что-то там.
Да, приведите это явно:
foo(static_cast<Y>(z));