prestokeys
prestokeys

Reputation: 4849

SFINAE used recursively

Consider this code:

#include <iostream>

struct A {
    void foo() const {std::cout << "A::foo()\n";}
};

struct B {
    A value;
    void foo() const {std::cout << "B::foo()\n";}
};

struct C {
    B value;
    void foo() const {std::cout << "C::foo()\n";}
};

struct D {
    C value;
};

struct Stop {
    Stop(...) {}
};

template <typename T>
void fooValue (const T& t, Stop) {
    t.foo();
}

template <typename T, typename = decltype(T::value)>
void fooValue (const T& t, int) {
    fooValue(t.value, 0);
}

template <typename T>
void foo (const T& t) {
    fooValue(t, 0);
}

int main() {
    const D d;
    foo(d);  // A::foo()
}

So A::foo() is outputted because A is the last type in the chain that has a value member. I want to now define

template <std::size_t N, typename T> void foo (const T&)

so that N = 0 will do the same thing, but N = 1 will have the second last type in the chain call its foo() function, N = 2 will have the third last type in the chain call its foo() function, etc... Thus in the example above, foo<1>(d); will output B::foo() and foo<2>(d); will output C::foo(). But I can't think of how to implement this. Can anyone help here?

Update: Thanks to Ryan Haining's discussion, I've come up with the following to compute the depth during compile time:

template <typename T>
using void_t = void;

template <typename T, std::size_t N, typename = void>
struct ChainLength : std::integral_constant<std::size_t, N> {};

template <typename T, std::size_t N>
struct ChainLength<T, N, void_t<decltype(T::value)>> : ChainLength<decltype(T::value), N+1> {};

Now we only need to use it properly.

Upvotes: 1

Views: 139

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275750

The plan is to do everything with functions instead of with types.

First, a wrapper for integral constant to reduce typing:

template<std::size_t I>
using index_t = std::integral_constant<std::size_t, I>;
template<std::size_t I>
constexpr index_t<I> index = {};

Now, some simple operations on it:

template<std::size_t A, std::size_t B>
constexpr index_t<A+B> add(index_t<A>, index_t<B>){ return {}; }

template<std::size_t A, std::size_t B>
constexpr index_t<A-B> sub(index_t<A>, index_t<B>){ return {}; }

template<std::size_t I>
constexpr index_t<I+1> next( index_t<I> ) { return {}; };

Calculate depth as an index:

template<class T>
constexpr index_t<0> fooIndex(const T& t, Stop) {
  return {};
}

template<class T, class = decltype(T::value)>
constexpr auto fooIndex(const T& t, int) {
  return next(fooIndex(t.value, 0));
}

Invoke at a specific depth:

template<class T>
void fooValue(const T& t, index_t<0>) {
  t.foo();
}
template<class T, std::size_t I>
void fooValue(const T& t, index_t<I>) {
  fooValue(t.value, index<I-1>);
}

Use them:

template <typename T>
void foo (const T& t) {
  auto idx = fooIndex(t, 0);
  return fooValue( t, sub( idx, index<1> ) ); 
}

Upvotes: 1

prestokeys
prestokeys

Reputation: 4849

This is thanks to the discussion with Ryan Haining:

#include <iostream>
#include <type_traits>

template <typename T>
using void_t = void;

template <typename T, std::size_t N, typename = void>
struct ChainLength : std::integral_constant<std::size_t, N> {};

template <typename T, std::size_t N>
struct ChainLength<T, N,
    void_t<decltype(std::declval<decltype(T::value)>().foo())>> :
    ChainLength<decltype(T::value), N+1> {};  // Here we check not only that T::data exists, but that decltype(T::data) has a foo() member function too.

template <std::size_t Count, typename T>
struct FooData {
    static void execute (const T& t) {
        FooData<Count-1, decltype(T::value)>::execute(t.value);
    }
};

template <typename T>
struct FooData<0, T> {
    static void execute (const T& t) { t.foo(); }
};

template <std::size_t N, std::size_t Length, typename T>
void fooHelper (const T& t, std::enable_if_t<(N < Length)>* = nullptr) {
    FooData<Length-N, T>::execute(t);
}

template <std::size_t N, std::size_t Length, typename T>
void fooHelper (const T&, std::enable_if_t<(N >= Length)>* = nullptr) {
    std::cout << "N value too big.\n";
}

template <std::size_t N, typename T>
void foo (const T& t) {
    fooHelper<N, ChainLength<T,0>::value>(t);
}

// Testing
struct A {
    void foo() const {std::cout << "A::foo()\n";}
};

struct B {
    A value;
    void foo() const {std::cout << "B::foo()\n";}
};

struct C {
    B value;
    void foo() const {std::cout << "C::foo()\n";}
};

struct D {
    C value;
};

int main() {
    const D d;
    foo<0>(d);  // A::foo()  (this is outputted because A is the last type in the chain that has a 'value' member)
    foo<1>(d);  // B::foo()  (this is outputted because B is the second last type in the chain that has a 'value' member)
    foo<2>(d);  // C::foo()  (this is outputted because C is the third last type in the chain that has a 'value' member)
    foo<3>(d);  // N value too big.
}

Upvotes: 1

Related Questions