sylvain
sylvain

Reputation: 35

Failing cleanly if we match a type using SFINAE

I'm toying with SFINAE and I try to generate a clean failure for three cases. The intend of UseInput is to switch an Input from IsFree to IsUsed. The Input types are packed inside an Inputs type (some kind of tuple). We access an Input by its index.

struct IsFree
{
};

struct IsUsed
{
};

template <typename Type, typename State>
struct Input
{
};

template <typename... DefaultInputs>
struct Inputs
{
};

template <typename Type, typename... DefaultInputs>
struct Inputs<Input<Type, IsFree>, DefaultInputs...>
    : public Inputs<DefaultInputs...>
{
};

template <typename Type, typename... DefaultInputs>
struct Inputs<Input<Type, IsUsed>, DefaultInputs...>
    : public Inputs<DefaultInputs...>
{
};

// UseInput creates a new Inputs type where an Input state is switched from IsFree to IsUsed
template <int index, typename LastInputs, typename NextInputs>
struct UseInput;

// We found the Input to switch
template <typename Type, typename... LastInputs, typename... NextInputs>
struct UseInput<0, Inputs<Input<Type, IsFree>, LastInputs...>, Inputs<NextInputs...>>
    : public UseInput<-1, Inputs<LastInputs...>, Inputs<NextInputs..., Input<Type, IsUsed>>>
{
};

// We try to switch an Input we shouldn't, this is an error
template <typename Type, typename... LastInputs, typename... NextInputs>
struct UseInput<0, Inputs<Input<Type, IsUsed>, LastInputs...>, Inputs<NextInputs...>>
{
};

// We processed the Inputs
template <int index, typename... NextInputs>
struct UseInput<index, Inputs<>, Inputs<NextInputs...>>
{
    typedef Inputs<NextInputs...> Result;
};

// Recursively process each Input
template <int index, typename Type, typename State, typename... LastInputs, typename... NextInputs>
struct UseInput<index, Inputs<Input<Type, State>, LastInputs...>, Inputs<NextInputs...>>
    : public UseInput<index - 1, Inputs<LastInputs...>, Inputs<NextInputs..., Input<Type, State>>>
{
};

These use cases work :

typedef Inputs<Input<float, IsFree>, Input<float, IsUsed>, Input<float, IsFree>> TestInputs;

typedef UseInput<0, TestInputs, Inputs<>>::Result Result0;
static_assert(std::is_same<Result0, Inputs<Input<float, IsUsed>, Input<float, IsUsed>, Input<float, IsFree>>>::value, "Unexpected type...");

typedef UseInput<2, TestInputs, Inputs<>>::Result Result2;
static_assert(std::is_same<Result2, Inputs<Input<float, IsFree>, Input<float, IsUsed>, Input<float, IsUsed>>>::value, "Unexpected type...");

These must fail :

typedef Inputs<Input<float, IsFree>, Input<float, IsUsed>, Input<float, IsFree>> TestInputs;

typedef UseInput<1, TestInputs, Inputs<>>::Result Result1;   // Already used Input

typedef UseInput<3, TestInputs, Inputs<>>::Result Result3;   // Bad index
typedef UseInput<-1, TestInputs, Inputs<>>::Result Result4;  //

The only solution I found is omit the typedef Inputs<> Result; but this seems bad way to report the errors and requires an additional index for testing the indices.

How can I fix my templates if I want a clean error reporting by the compiler ?

Upvotes: 1

Views: 98

Answers (1)

sehe
sehe

Reputation: 393134

I'd treat the list as a list, and iterate using the indices trick + parameter pack expansion.

Finally, a simple check that the resulting type isn't the same as the original will cover the case of "using" an input that was already used:

The "API"

template <typename Type, typename State> struct Input {};

template <typename Type> using Used = Input<Type, struct IsUsed>;
template <typename Type> using Free = Input<Type, struct IsFree>;

template <typename... Input> struct InputList {};

The Plumbing

Let's first make type functions to change state:

namespace detail {
    template <typename Type, typename State> struct ChangeStateF;
    template <typename Type, typename State> struct ChangeStateF<Input<Type, IsFree>, State> { using type = Input<Type, State>; };
    template <typename Type, typename State> struct ChangeStateF<Input<Type, IsUsed>, State> { using type = Input<Type, State>; };

Which we can alias for convenience:

    template <typename Input> using UseInput  = ChangeStateF<Input, IsUsed>;
    template <typename Input> using FreeInput = ChangeStateF<Input, IsFree>;

Now all we need is to conditionally apply such a change

    template <bool C, template <typename...> class F, typename T>
    using apply_if = std::conditional<C, typename F<T>::type, T>;

to all the elements in an InputList by index:

    template<size_t N, template <typename...> class F, typename... I, size_t... Idx>
    constexpr auto apply_at(InputList<I...>, std::index_sequence<Idx...>) {
        return InputList<typename apply_if<Idx == N, F, I>::type...>{};
    }

The index_sequence is a trivial type included (with C++14) and we can forward from an overload without it:

    template<size_t N, template <typename...> class F, typename... I>
    constexpr auto apply_at(InputList<I...> l) {
        return apply_at<N, F>(l, std::index_sequence_for<I...>{});
    }

Now to detect indices out of bounds or changes that didn't have any effect (e.g. the input was already in the required state):

    template<size_t N, template <typename...> class F, typename... I>
    constexpr auto mutate_at(InputList<I...> l) {
        auto r = apply_at<N, F>(l);
        static_assert(not std::is_same<decltype(r), decltype(l)>{}, "Invalid Mutation");
        return r;
    }

Wait, We Don't Want Functions, We Want Types!

This is 'modern style' meta programming, let's leave the detail namespace:

    template<size_t N, typename L>
    constexpr auto use_at(L x) { return mutate_at<N, UseInput>(x); }

    template<size_t N, typename L>
    constexpr auto free_at(L x) { return mutate_at<N, FreeInput>(x); }
}

Now, we're back in "type land":

template <std::size_t N, typename List> using Use   = decltype(detail::use_at<N>(List{}));
template <std::size_t N, typename List> using Clear = decltype(detail::free_at<N>(List{}));

DEMO

Live On Coliru

#include <iostream>
#include <typeinfo>

int main() { 
    using Inputs = InputList<Free<float>, Used<float>, Used<float> >;
    using New = Use<0, Inputs>;
    //using Oops = Use<0, New>; doesn't compile

    std::cout << typeid(Inputs).name() << "\n";
    std::cout << typeid(New).name() << "\n";
}

Prints

InputList<Input<float, IsFree>, Input<float, IsUsed>, Input<float, IsUsed> >
InputList<Input<float, IsUsed>, Input<float, IsUsed>, Input<float, IsUsed> >

Uncommenting the line with Oops would lead to

main.cpp:36:9: error: static_assert failed "Invalid Mutation"
        static_assert(not std::is_same<decltype(r), decltype(l)>{}, "Invalid Mutation");

Upvotes: 1

Related Questions