Germán Diago Germán Diago - 3 months ago 12
C++ Question

Metaprogramming tricks: how to simplify implementation of two metafunctions

I am writing some program to call some APIs automatically through code generation.

In some cases I need to convert from a type

Source
, to a type
Target
, but these types come decorated with pointers, const, etc. So what I need to do is to remove all decorations such as pointer, const, array, etc, get the plain type to map it to another type, and later, apply the decorations back into the new type.

The implementation has lots of template specializations. Questions after the code. I cannot use
constexpr
metaprogramming because I need to make it work with VS2013.

template <class T>
struct TypeIs {
using type = T;
};

template <class T>
struct GetPlainType : TypeIs<typename std::decay<T>::type> {};

template <class T>
struct GetPlainType<T&> : TypeIs<typename GetPlainType<T>::type> {};

template <class T>
struct GetPlainType<T const &> : TypeIs<typename GetPlainType<T>::type> {};

template <class T>
struct GetPlainType<T &&> : TypeIs<typename GetPlainType<T>::type> {};

template <class T>
struct GetPlainType<T const &&> : TypeIs<typename GetPlainType<T>::type> {};

template <class T>
struct GetPlainType<T*> : TypeIs<typename GetPlainType<T>::type> {};

template <class T>
struct GetPlainType<T const *> : TypeIs<typename GetPlainType<T>::type> {};

template <class T>
struct GetPlainType<T const> : TypeIs<typename GetPlainType<T>::type> {};

template <class T>
struct GetPlainType<T[]> : TypeIs<typename GetPlainType<T>::type> {};


template <class T>
struct GetPlainType<T const[]> : TypeIs<typename GetPlainType<T>::type> {};

template <class T, std::size_t I>
struct GetPlainType<T[I]> : TypeIs<typename GetPlainType<T>::type> {};

template <class T, std::size_t I>
struct GetPlainType<T const [I]> : TypeIs<typename GetPlainType<T>::type> {};


template <class T>
using GetPlainType_t = typename GetPlainType<T>::type;


template <class Decorated, class Plain>
struct CopyDecorations : TypeIs<Plain> {};


template <class T, class Plain>
struct CopyDecorations<T const, Plain> :
TypeIs<typename CopyDecorations<T, Plain const>::type> {};


template <class T, class Plain>
struct CopyDecorations<T *, Plain> :
TypeIs<typename CopyDecorations<T, Plain *>::type> {};

template <class T, class Plain>
struct CopyDecorations<T const *, Plain> :
TypeIs<typename CopyDecorations<T, Plain const *>::type> {};


template <class T, class Plain>
struct CopyDecorations<T &, Plain> :
TypeIs<typename CopyDecorations<T, Plain &>::type> {};

template <class T, class Plain>
struct CopyDecorations<T const &, Plain> :
TypeIs<typename CopyDecorations<T, Plain const &>::type> {};

template <class T, class Plain>
struct CopyDecorations<T &&, Plain> :
TypeIs<typename CopyDecorations<T, Plain &&>::type> {};


template <class T, class Plain>
struct CopyDecorations<T const &&, Plain> :
TypeIs<typename CopyDecorations<T, Plain const &&>::type> {};


template <class T, class Plain>
struct CopyDecorations<T[], Plain> :
TypeIs<typename CopyDecorations<T, Plain[]>::type> {};

template <class T, class Plain>
struct CopyDecorations<T const [], Plain> :
TypeIs<typename CopyDecorations<T, Plain const []>::type> {};


template <class T, class Plain, std::size_t I>
struct CopyDecorations<T [I], Plain> :
TypeIs<typename CopyDecorations<T, Plain[I]>::type> {};

template <class T, class Plain, std::size_t I>
struct CopyDecorations<T const [I], Plain> :
TypeIs<typename CopyDecorations<T, Plain const [I]>::type> {};


template <class Decorated, class Plain>
using CopyDecorations_t = typename CopyDecorations<Decorated, Plain>::type;


int main()
{
static_assert(std::is_same<GetPlainType_t<int>, int>{}, "");
static_assert(std::is_same<GetPlainType_t<int const>, int>{}, "");
static_assert(std::is_same<GetPlainType_t<int *>, int>{}, "");
static_assert(std::is_same<GetPlainType_t<int **>, int>{}, "");
static_assert(std::is_same<GetPlainType_t<int * &>, int>{}, "");
static_assert(std::is_same<GetPlainType_t<int ** &>, int>{}, "");
static_assert(std::is_same<GetPlainType_t<int const * []>, int>{}, "");
static_assert(std::is_same<GetPlainType_t<int const **[][3][5]>, int>{}, "");


static_assert(std::is_same<CopyDecorations_t<int, double>, double>{}, "");
static_assert(std::is_same<CopyDecorations_t<int const, double>, double const>{}, "");
static_assert(std::is_same<CopyDecorations_t<int *, double>, double *>{}, "");
static_assert(std::is_same<CopyDecorations_t<int **, double>, double **>{}, "");
static_assert(std::is_same<CopyDecorations_t<int[], double>, double[]>{}, "");
static_assert(std::is_same<CopyDecorations_t<int[3], double>, double[3]>{}, "");


//******************THE TESTS BELOW DO NOT WORK
//static_assert(std::is_same<CopyDecorations_t<int[][3], double>, double[][3]>{}, "");
//static_assert(std::is_same<CopyDecorations_t<int * &, double>, double * &>{}, "");
// static_assert
// (
//std::is_same<CopyDecorations_t<int const * [], double>,
// double const * []>{}, "");
// static_assert
// (std::is_same<CopyDecorations_t<int const **[][3][5], double>,
// double const **[][3][5]>{}, "");
}


Questions:


  1. Can I simplify the implementation?

  2. The tests that fail (see
    main
    function), how can I fix them?

  3. In which cases (ignoring volatile and pointers to members, pointers to functions, and functions). can you think my implementation will fail?


Answer

I found this question one of the most interesting one about C++ metaprogramming on SO indeed.
I enjoyed trying to find a proper solution. Thank you. :-)


It follows a minimal, working example.
It is not complete, but it gives an idea of a possible approach to be used to do that.
The function f (ok, you can choose a better name in your code) accepts two template parameters: the type to be cleaned and the one to be decorated.
It returns a template type (types) that introduces two using declarations, basic and decorated, with the first template parameter cleaned up as basic and the second one decorated as decorated.
It does all at once (cleaning up and decoration). You can still use only the first parameter, in this case decorated is defaulted to a decorated char type.

Here is the full code:

#include<type_traits>
#include<cstddef>

static constexpr std::size_t N = 42;

template<std::size_t N>
struct choice: choice<N-1> {};

template<>
struct choice<0> {};

template<typename T, typename U>
struct types {
    using basic = T;
    using decorated = U;
};

template<typename T, typename U>
constexpr auto
f(choice<0>) { return types<T, U>{}; }

template<typename T, typename U,
    typename = std::enable_if_t<std::is_pointer<T>::value>>
constexpr auto f(choice<1>) {
    auto t = f<std::remove_pointer_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::add_pointer_t<D>>{};
}

template<typename T, typename U,
    typename = std::enable_if_t<std::is_lvalue_reference<T>::value>>
constexpr auto f(choice<2>) {
    auto t = f<std::remove_reference_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::add_lvalue_reference_t<D>>{};
}

template<typename T, typename U,
    typename = std::enable_if_t<std::is_rvalue_reference<T>::value>>
constexpr auto f(choice<3>) {
    auto t = f<std::remove_reference_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::add_rvalue_reference_t<D>>{};
}

template<typename T, typename U,
    typename = std::enable_if_t<std::is_array<T>::value>>
constexpr auto f(choice<4>) {
    auto t = f<std::remove_extent_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::conditional_t<(0==std::extent<T>::value), D[], D[std::extent<T>::value]>>{};
}

template<typename T, typename U,
    typename = std::enable_if_t<std::is_const<T>::value>>
constexpr auto f(choice<5>) {
    auto t = f<std::remove_const_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::add_const_t<D>>{};
}

template<typename T, typename U,
    typename = std::enable_if_t<std::is_volatile<T>::value>>
constexpr auto f(choice<6>) {
    auto t = f<std::remove_volatile_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::add_volatile_t<D>>{};
}

template<typename T, typename U = char>
constexpr auto f() {
    return f<T, U>(choice<N>{});
}

int main() {
    // something complex to show that it seems to work
    static_assert(std::is_same<
        decltype(f<const int ** const &&, char>()),
        types<int, const char ** const &&>
    >::value, "!");

    // some of the OP's examples (the most interesting)
    static_assert(std::is_same<decltype(f<int, int>()), types<int, int>>::value, "!");
    static_assert(std::is_same<decltype(f<int const, int>()), types<int, int const>>::value, "!");
    static_assert(std::is_same<decltype(f<int *, int>()), types<int, int *>>::value, "!");
    static_assert(std::is_same<decltype(f<int **, double>()), types<int, double **>>::value, "!");
    static_assert(std::is_same<decltype(f<int *&, int>()), types<int, int *&>>::value, "!");
    static_assert(std::is_same<decltype(f<int **&, float>()), types<int, float **&>>::value, "!");
    static_assert(std::is_same<decltype(f<int [3], char>()), types<int, char [3]>>::value, "!");
    static_assert(std::is_same<decltype(f<int [], int>()), types<int, int []>>::value, "!");
    static_assert(std::is_same<decltype(f<int [][3], double>()), types<int, double [][3]>>::value, "!");
    static_assert(std::is_same<decltype(f<int const **[][3][5], int>()), types<int, int const **[][3][5]>>::value, "!");

    // of course, you don't need to provide the second type if you don't need it
    // in this case, types::decorated is defaulted to a decorated char type
    f<int const **[][3][5]>();
}

Set apart the fact that it won't compile without the constexprs because of the static_asserts, you can freely remove them and use the function at runtime.

Actually, it could be turned maybe to a definition-less solution, providing the right return types to the declarations and using a bunch of decltypes, but I suspect that it would be far from being readable.

EDIT

As mentioned by the OP, he doesn't want (or at least, he cannot use) constexprs.
It follows a slightly different solution, still based on the previous one.
The basic idea is to use f as an unevaluated operand with decltype.
Here is the full code:

#include<type_traits>
#include<cstddef>

static const std::size_t N = 42;

template<std::size_t N>
struct choice: choice<N-1> {};

template<>
struct choice<0> {};

template<typename T, typename U>
struct types {
    using basic = T;
    using decorated = U;
};

template<typename T, typename U>
auto f(choice<0>) { return types<T, U>{}; }

template<typename T, typename U,
    typename = std::enable_if_t<std::is_pointer<T>::value>>
auto f(choice<1>) {
    auto t = f<std::remove_pointer_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::add_pointer_t<D>>{};
}

template<typename T, typename U,
    typename = std::enable_if_t<std::is_lvalue_reference<T>::value>>
auto f(choice<2>) {
    auto t = f<std::remove_reference_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::add_lvalue_reference_t<D>>{};
}

template<typename T, typename U,
    typename = std::enable_if_t<std::is_rvalue_reference<T>::value>>
auto f(choice<3>) {
    auto t = f<std::remove_reference_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::add_rvalue_reference_t<D>>{};
}

template<typename T, typename U,
    typename = std::enable_if_t<std::is_array<T>::value>>
auto f(choice<4>) {
    auto t = f<std::remove_extent_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::conditional_t<(0==std::extent<T>::value), D[], D[std::extent<T>::value]>>{};
}

template<typename T, typename U,
    typename = std::enable_if_t<std::is_const<T>::value>>
auto f(choice<5>) {
    auto t = f<std::remove_const_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::add_const_t<D>>{};
}

template<typename T, typename U,
    typename = std::enable_if_t<std::is_volatile<T>::value>>
auto f(choice<6>) {
    auto t = f<std::remove_volatile_t<T>, U>(choice<N>{});
    using B = typename decltype(t)::basic;
    using D = typename decltype(t)::decorated;
    return types<B, std::add_volatile_t<D>>{};
}

template<typename T, typename U>
auto f() {
    return f<T, U>(choice<N>{});
}

template<typename T, typename U = char>
using my_type = decltype(f<T, U>());

template<typename T, typename U = char>
using my_type_basic_t = typename decltype(f<T, U>())::basic;

template<typename T, typename U = char>
using my_type_decorated_t = typename decltype(f<T, U>())::decorated;

int main() {
    int i = 42;
    my_type_decorated_t<char *, int> ptr = &i;
    // of course, it can still be used in a constant expression if needed
    // constexpr my_type_decorated_t<char *, int> ptr = nullptr;

}
Comments