Reputation: 4335
I want to generate type-safe conversions between two sets of types using templates. The base case looks like this:
template <typename T> struct ATraits {};
template <typename T> struct BTraits {};
template <> struct ATraits<BFoo> { using AType = AFoo; };
template <> struct ATraits<BBar> { using AType = ABar; };
template <> struct BTraits<AFoo> { using BType = BFoo; };
template <> struct BTraits<ABar> { using BType = BBar; };
template <typename TB>
auto AFromB(TB x) -> typename ATraits<TB>::AType {
return static_cast<typename ATraits<TB>::AType>(x);
}
template <typename TA>
auto BFromA(TA x) -> typename BTraits<TA>::BType {
return static_cast<typename BTraits<TA>::BType>(x);
}
The above works for basic types. Now I want to extend this to pointers and const-qualified types. It works when I define the following partial specializations:
template <typename T> struct ATraits<T*> {
using AType = typename ATraits<T>::AType*;
}
template <typename T> struct BTraits<T*> {
using BType = typename BTraits<T>::BType*;
}
template <typename T> struct ATraits<const T> {
using AType = const typename ATraits<T>::AType;
}
template <typename T> struct BTraits<const T> {
using BType = const typename BTraits<T>::BType;
}
However, this seems like a lot of boilerplate. Is there a more concise way (maybe involving type traits) to define this type mapping for pointers, references, cv-qualified types, etc.?
Upvotes: 0
Views: 79
Reputation: 275896
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.
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);
}
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.
Upvotes: 1