There is going to be a lot of boilerplate.
I'd actually propose using a different system than template specialization.
template<class T>struct tag_t{using type=T;};
template<class T>constexpr tag_t<T> tag{};
template<class Tag>using type_t=typename Tag::type;
struct const_t {}; constexpr const_t const_v{};
struct volatile_t {}; constexpr volatile_t volatile_v{};
struct ptr_t {}; constexpr ptr_t ptr_v{};
struct lref_t {}; constexpr lref_t lref_v{};
struct rref_t {}; constexpr rref_t rref_v{};
struct retval_t{}; constexpr retval_t retval_v{};
struct func_t{}; constexpr func_t func_v{};
template<class Sig>
struct func_builder_t{}; template<class Sig> constexpr func_builder_t<Sig> func_builder_v{};
now an algebra:
template<class T>
constexpr tag_t<T&> operator+( tag_t<T>,lref_t ) { return {}; }
template<class T>
constexpr tag_t<T&&> operator+( tag_t<T>,rref_t ) { return {}; }
template<class T>
constexpr tag_t<T*> operator+( tag_t<T>,ptr_t ) { return {}; }
template<class T>
constexpr tag_t<T const> operator+( tag_t<T>,const_t ) { return {}; }
template<class T>
constexpr tag_t<T volatile> operator+( tag_t<T>,volatile_t ) { return {}; }
template<class T>
constexpr func_builder_t<T()> operator+(tag_t<T>,retval_t){ return {}; }
template<class R, class...Ts, class T0, class T1>
constexpr func_builder_t<R(T1,Ts...,T0)> operator+(func_builder_t<R(T0,Ts...)>,tag_t<T1>){ return {}; }
template<class R, class T0>
constexpr func_builder_t<R(T0)> operator+(func_builder_t<R()>,tag_t<T0>){ return {}; }
template<class R, class...Ts, class T0>
constexpr tag_t<R(Ts...,T0)> operator+(func_builder_t<R(T0,Ts...)>,func_t){ return {}; }
template<class R, class...Ts, class T0, class Rhs>
constexpr auto operator+(func_builder_t<R(T0,Ts...)>,Rhs rhs){
return func_builder_v<R(Ts...)>+(tag<T0>+rhs);
}
next we can decompose something:
template<class T>
constexpr std::tuple<tag_t<T>> decompose( tag_t<T> ) { return {}; }
template<class T>
constexpr auto decompose( tag_t<T*> ) {
return std::tuple_cat( decompose(tag<T>), std::make_tuple( ptr_v ) );
}
template<class T>
constexpr auto decompose( tag_t<T&> ) {
return std::tuple_cat( decompose(tag<T>), std::make_tuple( lref_v ) );
}
template<class T>
constexpr auto decompose( tag_t<T&&> ) {
return std::tuple_cat( decompose(tag<T>), std::make_tuple( rref_v ) );
}
template<class T>
constexpr auto decompose( tag_t<T const> ) {
return std::tuple_cat( decompose(tag<T>), std::make_tuple( const_v ) );
}
template<class T>
constexpr auto decompose( tag_t<T volatile> ) {
return std::tuple_cat( decompose(tag<T>), std::make_tuple( volatile_v ) );
}
template<class T>
constexpr auto decompose( tag_t<T const volatile> ) {
return std::tuple_cat( decompose(tag<T>), std::make_tuple( const_v, volatile_v ) );
}
template<class R, class...Args>
constexpr auto decompose( tag_t<R(Args...)> ) {
constexpr auto args = std::tuple_cat( decompose(tag<Args>)... );
return std::tuple_cat( decompose(tag<R>), std::make_tuple(retval_v), args, std::make_tuple(func_v) );
}
template<class...Ts>
constexpr auto compose( std::tuple<Ts...> ) {
return (... + Ts{});
}
now we can take a type:
struct X;
tag<X * const volatile *>
and do
auto decomp0 = decompose(tag<X * const volatile *>);
where decomp is of type
std::tuple< tag_t<X>, ptr_t, const_t, volatile_t, ptr_t > tup0 = decomp0;
auto decomp1 = decompose(tag<int(double, char)>);
std::tuple< tag_t<int>, retval_t, tag_t<double>, tag_t<char>, func_t > tup1 = decomp1;
tag_t<int(double, char)> tag_test = compose( decomp1 );
std::tuple< tag_t<int>, retval_t, tag_t<int>, func_t, ptr_t > tup_test_2 = decompose( tag<int(*)(int)> );
tag_t<int(*)(int)> tag_test_3 = compose( tup_test_2 );
we can go further with this, including supporting function signatures, sized and unsized, arrays, etc.
Then we write a function on tag_t<T>
that maps to the type we want.
Next, we decompose the incoming type, remap only the tag_t's in the tuple, then sum up the tuple using a fold expression and std::apply
.
But I'm crazy.
The only benefit of this is that you can (A) reuse the decomposition/recomposition code, and (B) can distribute the type mappings to the namespaces of the types you are working with, as the map function on tag_t will look up the function name in the namespace of the tagged type.
Live example.
We can then use this (rather complex) machinery to solve your problem.
template<class T>
constexpr auto ATypeFromB( tag_t<T> ) {
return tag< typename ATraits<T>::AType >;
}
template<class T>
constexpr auto BTypeFromA( tag_t<T> ) {
return tag< typename BTraits<T>::BType >;
}
template<class F, class T>
constexpr auto map_tags_only( F&& f, tag_t<T> t ) {
return f(t);
}
template<class F, class O>
constexpr auto map_tags_only( F&& f, O o ) {
return o;
}
template <typename TB>
auto AFromB(TB x) {
auto decomp = decompose( tag<TB> );
auto mapped = std::apply( [](auto...elements) {
return std::make_tuple(
map_tags_only( [](auto x){return ATypeFromB(x);}, elements )...
);
}, decomp );
auto comp = compose(mapped);
using R = typename decltype(comp)::type;
return static_cast<R>(x);
}
template <typename TA>
auto BFromA(TA x) {
auto decomp = decompose( tag<TA> );
auto mapped = std::apply( [](auto...elements) {
return std::make_tuple(
map_tags_only( [](auto x){return BTypeFromA(x);}, elements )...
);
}, decomp );
auto comp = compose(mapped);
using R = typename decltype(comp)::type;
return static_cast<R>(x);
}
Live example.
Again, the one and only advantage is that all of the mess with tearing apart const, functions, arrays, blah blah blah, gets done once in this system. And you can reuse it somewhere else (in this case I used it twice).
It naturally gets much worse when we extend it to member functions (which by themselves require like 100 specializations to cover a bunch of cases), assuming we want to remap those too.