Sunday, January 12, 2014

Type Functions in C++

C++ metafunctions normally don't look like functions. What if this changed with C++11/C++14? What if metafunctions can be C++ functions? I decided to try it out.

I decided to begin with type functions. We want functions which take types as arguments and return types. Here's a type container for these functions to operate on.

template<class T>
struct Type
{
    using type = T;
};
Let's try something easy. ptr() turns a T into a T*.
template<class T>
auto ptr(Type<T>)
{
    return Type<T*>{};
}
Let's use this to declare an int**.
void testPtr1()
{
    int                                   i{};
    int*                                  p{&i};
    decltype(ptr(ptr(Type<int>{})))::type pp{&p};

    assert(typeid(pp) == typeid(int**));
}
We only "call" ptr() within decltype(); this prevents it from generating any runtime code, even in debug builds. This isn't very readable, so let's simplify the interface a bit.
static const Type<int> IntType;

#define EXTRACT_TYPE(e) decltype(e)::type

void testPtr2()
{
    int                             i{};
    int*                            p{&i};
    EXTRACT_TYPE(ptr(ptr(IntType))) pp{&p};

    assert(typeid(pp) == typeid(int**));
}
We established an approach for defining and using a type function; let's see how well this works with pattern matching.
template<class T>
auto removeCV(Type<T>)
{
    return Type<T>{};
}

template<class T>
auto removeCV(Type<const T>)
{
    return Type<T>{};
}

template<class T>
auto removeCV(Type<volatile T>)
{
    return Type<T>{};
}

template<class T>
auto removeCV(Type<const volatile T>)
{
    return Type<T>{};
}

void testRemoveCV1()
{
    EXTRACT_TYPE(ptr(removeCV(Type<int>{})))
        p1{};
    EXTRACT_TYPE(ptr(removeCV(Type<const int>{})))
        p2{};
    EXTRACT_TYPE(ptr(removeCV(Type<volatile int>{})))
        p3{};
    EXTRACT_TYPE(ptr(removeCV(Type<const volatile int>{})))
        p4{};
    EXTRACT_TYPE(ptr(Type<const volatile int>{}))
        p5{};

    assert(typeid(p1) == typeid(int*));
    assert(typeid(p2) == typeid(int*));
    assert(typeid(p3) == typeid(int*));
    assert(typeid(p4) == typeid(int*));
    assert(typeid(p5) == typeid(const volatile int*));
}
That works. Let's simplify our test case a bit.
#define ASSERT_EXTRACT_TYPE(a, b) \
    assert(typeid(EXTRACT_TYPE(a)) == typeid(b))

void testRemoveCV2()
{
    ASSERT_EXTRACT_TYPE(
        ptr(removeCV(Type<int>{})),
        int*);
    ASSERT_EXTRACT_TYPE(
        ptr(removeCV(Type<const int>{})),
        int*);
    ASSERT_EXTRACT_TYPE(
        ptr(removeCV(Type<volatile int>{})),
        int*);
    ASSERT_EXTRACT_TYPE(
        ptr(removeCV(Type<const volatile int>{})),
        int*);
    ASSERT_EXTRACT_TYPE(
        ptr(Type<const volatile int>{}),
        const volatile int*);
}
Let's try recursion. We want a function which retrieves the nth argument type from a function type.
template<int i>
using int_ = integral_constant<int, i>;

// Found it
template<class R, class A0, class... A>
auto argn(int_<0>, Type<R(A0, A...)>)
{
    return Type<A0>{};
}

// Continue search
template<int n, class R, class A0, class... A>
auto argn(int_<n>, Type<R(A0, A...)>)
{
    return argn(int_<n - 1>{}, Type<R(A...)>{});
}

void testArgn1()
{
    ASSERT_EXTRACT_TYPE(
        argn(int_<0>{}, Type<void(int, double, float)>{}),
        int);
    ASSERT_EXTRACT_TYPE(
        argn(int_<1>{}, Type<void(int, double, float)>{}),
        double);
    ASSERT_EXTRACT_TYPE(
        argn(int_<2>{}, Type<void(int, double, float)>{}),
        float);
}
argn(int_<n>{}, ...) seems a little silly; let's give it a nicer interface.
template<int n, class T>
auto argn(Type<T> t)
{
    return argn(int_<n>{}, t);
}

void testArgn2()
{
    ASSERT_EXTRACT_TYPE(
        argn<0>(Type<double(int, double, float)>{}), 
        int);
    ASSERT_EXTRACT_TYPE(
        argn<1>(Type<double(int, double, float)>{}), 
        double);
    ASSERT_EXTRACT_TYPE(
        argn<2>(Type<double(int, double, float)>{}), 
        float);
}
Let's try conditionals.
template<class T, class F>
auto if_(true_type, T t, F)
{
    return t;
}

template<class T, class F>
auto if_(false_type, T, F f)
{
    return f;
}

// demoIf's return type depends on Cond
template<class Cond>
auto demoIf(Cond c)
{
    return if_(c, Type<int>{}, Type<double**>{});
}

// Let's give the compiler more work to do
template<class Cond1, class Cond2>
auto demoNestedIf(Cond1 c1, Cond2 c2)
{
    return if_(c1,
        if_(c2,
            Type<char>{},
            Type<short>{}),
        if_(c2,
            Type<int>{},
            Type<long>{}));
}

void testIf()
{
    ASSERT_EXTRACT_TYPE(demoIf(true_type{}),  int);
    ASSERT_EXTRACT_TYPE(demoIf(false_type{}), double**);

    ASSERT_EXTRACT_TYPE(
        demoNestedIf(true_type{},  true_type{}),
        char);
    ASSERT_EXTRACT_TYPE(
        demoNestedIf(true_type{},  false_type{}),
        short);
    ASSERT_EXTRACT_TYPE(
        demoNestedIf(false_type{}, true_type{}),
        int);
    ASSERT_EXTRACT_TYPE(
        demoNestedIf(false_type{}, false_type{}),
        long);
}
We could try a type list at this point, but I'm feeling a bit ambitious. Let's try a type map.
// Each KV must be a pair. first_type and second_type must be
// default-constructable and copyable. Type<...> works well as a
// key or a value.
template<class... KV>
struct Map
{
};

// Match found
template<class K0, class V0, class... KV>
auto at(Map<pair<K0, V0>, KV...>, K0)
{
    return V0{};
}

// Continue search. Generates a compiler error if K is not
// found.
template<class KV0, class... KV, class K>
auto at(Map<KV0, KV...>, K k)
{
    return at(Map<KV...>{}, k);
}

void testMapAt()
{
    using M = Map<
        pair<int_<4>,    Type<double>>,
        pair<Type<int*>, Type<int>>,
        pair<Type<int>,  Type<int*>>>;

    ASSERT_EXTRACT_TYPE(at(M{}, int_<4>{}),    double);
    ASSERT_EXTRACT_TYPE(at(M{}, Type<int*>{}), int);
    ASSERT_EXTRACT_TYPE(at(M{}, Type<int>{}),  int*);
}
erase() takes a bit more work.
template<class KV0, class... KV1>
auto prepend(KV0, Map<KV1...>)
{
    return Map<KV0, KV1...>{};
}

// Key found
template<class K0, class V0, class... KV>
auto erase(Map<pair<K0, V0>, KV...>, K0)
{
    return Map<KV...>{};
}

// Continue search
template<class KV0, class... KV, class K>
auto erase(Map<KV0, KV...>, K k)
{
    return prepend(KV0{}, erase(Map<KV...>{}, k));
}

// End of map
template<class K>
auto erase(Map<>, K)
{
    return Map<>{};
}

void testMapErase()
{
    using M = Map<
        pair<int_<4>, Type<double>>,
        pair<Type<int*>, Type<int>>,
        pair<Type<int>, Type<int*>>>;

    // key not found
    assert(
        typeid(erase(M{}, int_<2>{})) ==
        typeid(M));

    // remove row 0, 1
    assert(
        typeid(erase(erase(M{}, int_<4>{}), Type<int*>{})) ==
        typeid(Map<pair<Type<int>, Type<int*>>>));

    // remove row 0, 2
    assert(
        typeid(erase(erase(M{}, int_<4>{}), Type<int>{})) ==
        typeid(Map<pair<Type<int*>, Type<int>>>));

    // remove row 2, 1
    assert(
        typeid(erase(erase(M{}, Type<int>{}), Type<int*>{})) ==
        typeid(Map<pair<int_<4>, Type<double>>>));
}
I'll leave insert() as an exercise.

I like how these type functions turned out; they seem a little less messy than the template struct approach. There's a lot more to template metaprogramming than just manipulating types; I plan to keep exploring.

#include <assert.h>
#include <iostream>
#include <type_traits>
#include <typeinfo>
#include <utility>

using namespace std;

... code snippets from above ...

int main()
{
    testPtr1();
    testPtr2();
    testRemoveCV1();
    testRemoveCV2();
    testArgn1();
    testArgn2();
    testIf();
    testMapAt();
    testMapErase();

    cout << "Tests passed" << endl;
}
Full source at ideone

No comments:

Post a Comment