ritter
ritter

Reputation: 7719

Type sensitive tuple visitor

Suppose I have a std::tuple made up of types like

struct A {
  static void tip();
};

struct B {
  static void tip();
};

struct Z {
};

std::tuple<const A&,const B&,const Z&> tpl;

Yes, I need separate A, B. (The implementation of ::tip() differs for each type.) What I try to implement is a type-sensitive "visitor" that iterates through the tuple starting from the beginning to the end. Upon visiting a particular element of type T a function should be called depending on whether T has the ::tip() method or not. In the simple example of above only A and B have ::tip() implemented and Z not. So, the iterator should call twice the function for types with the ::tip() method and once the other function.

Here is what I came up with:

template< int N , bool end >
struct TupleIter
{
  template< typename T , typename... Ts >
  typename std::enable_if< std::is_function< typename T::tip >::value , void >::type
  static Iter( const T& dummy , const std::tuple<Ts...>& tpl ) {
    std::cout << "tip\n";
    std::get<N>(tpl); // do the work
    TupleIter<N+1,sizeof...(Ts) == N+1>::Iter( std::get<N+1>(tpl) , tpl );
  }

  template< typename T , typename... Ts >
  typename std::enable_if< ! std::is_function< typename T::tip >::value , void >::type
  static Iter( const T& dummy , const std::tuple<Ts...>& tpl ) {
    std::cout << "no tip\n";
    std::get<N>(tpl); // do the work
    TupleIter<N+1,sizeof...(Ts) == N+1>::Iter( std::get<N+1>(tpl) , tpl );
  }
};


template< int N >
struct TupleIter<N,true>
{
  template< typename T , typename... Ts >
  static void Iter( const std::tuple<Ts...>& tpl ) {
    std::cout << "end\n";
  }
};

I use a dummy instance of the type of the element at the iterator position and decide via enable_if which function to call. Unfortunately this doesn't work/isn't a nice solution:

  1. The compiler complains about recursive instantiation
  2. The const T& dummy is not a clean solution

I was wondering if enable_if is the right strategy to do the decision and how can one recursively iterate through the std::tuple capturing the first type and keeping all the remaining arguments in vital state. Read through How to split a tuple? but it doesn't do any decision.

How can one implement such a thing in a correct and portable way in C++11?

Upvotes: 3

Views: 1448

Answers (2)

Arzar
Arzar

Reputation: 14317

Here is another take on the question, very similar to mfontanini answer, but showcasing:

boost::fusion::for_each (instead of manually iterate over the tuple).
A variant for implementing has_type using an expression-based SFINAE approach, that I feel a little bit simpler to follow than the usual sizeof trick.

#include <boost/tuple/tuple.hpp>
#include <boost/fusion/include/boost_tuple.hpp>
#include <boost/fusion/algorithm.hpp>

#include <iostream>

    struct nat // not a type
    {
    private:
        nat(); 
        nat(const nat&);
        nat& operator=(const nat&);
        ~nat();
    };

    template <typename T>
    struct has_tip
    {
        static auto has_tip_imp(...) -> nat;

        template <typename U>
        static auto has_tip_imp(U&&) -> decltype(U::tip());

        typedef decltype(has_tip_imp(std::declval<T>())) type;
        static const bool value = !std::is_same<type, nat>::value;
    };


    struct CallTip
    {
        template<typename T>
        typename std::enable_if<has_tip<T>::value>::type
        operator()(T& t) const
        {
            std::cout << "tip\n";
            T::tip();
        }

        template<typename T>
        typename std::enable_if<!has_tip<T>::value>::type
        operator()(T& t) const
        {
            std::cout << "no tip\n";
            return;
        }
    };

struct A {
    static void tip(){}
};

struct B {
    static void tip(){}
};

struct Z {
};

int main()
{
    A a;
    B b;
    Z z;
    boost::tuple<const A&,const B&,const Z&> tpl(a, b, z);
    boost::fusion::for_each(tpl, CallTip());
}

Note that if your compiler support variadic template you can use std::tuple instead of boost::tuple inside fusion::for_each by including #include<boost/fusion/adapted/std_tuple.hpp>

Edit : As pointed by Xeo in the comment, it is possible to simplify a lot the expression-SFINAE approach by removing completely the trait has_tip and simply forward to a little call helper.
The final code is really neat and tight !

#include <boost/tuple/tuple.hpp>
#include <boost/fusion/include/boost_tuple.hpp>
#include <boost/fusion/algorithm.hpp>

#include <iostream>

struct CallTip
{
    template<typename T>
    void operator()(const T& t) const
    {
        call(t);
    }

    template<class T>
    static auto call(const T&) -> decltype(T::tip())
    { 
        std::cout << "tip\n";
        T::tip(); 
    }

    static void call(...)
    {
        std::cout << "no tip\n";
    }
};

struct A {
    static void tip(){}
};

struct B {
    static void tip(){}
};

struct Z {
};

int main()
{
    A a;
    B b;
    Z z;
    boost::tuple<const A&,const B&,const Z&> tpl(a, b, z);
    boost::fusion::for_each(tpl, CallTip());
}

Upvotes: 2

mfontanini
mfontanini

Reputation: 21910

Well, it was harder than I expected, but this works.

Some things you were doing wrong/that I modified:

  1. You can't evaluate this: std::is_function< typename T::tip >::value, since T::tip is not a type. Even if this could be evaluated, what would happen when T::tip does not exist? Substitution would still fail.
  2. Since you use const references as your tuple's inner types, you had to clean them before trying to find the tip member inside them. By cleaning I mean removing const and removing the reference.
  3. That dummy type stuff was not a good idea, there was no need to use that parameter. You can achieve the same thing using std::tuple_element, which retrieves the i-th type from a tuple.
  4. I modified TupleIter's template parameters to the following, which means:

"TupleIter that processes the index-th type, inside a tuple of size n".

template<size_t index, size_t n> 
struct TupleIter;

The whole code is this:

#include <tuple>
#include <iostream>
#include <type_traits>

struct A {
  static void tip();
};

struct B {
  static void tip();
};

struct Z {
};

// Indicates whether the template parameter contains a static member named tip.
template<class T>
struct has_tip {
    template<class U>
    static char test(decltype(&U::tip));

    template<class U>
    static float test(...);

    static const bool value = sizeof(test<typename std::decay<T>::type>(0)) == sizeof(char);
};

// Indicates whether the n-th type contains a tip static member
template<size_t n, typename... Ts>
struct nth_type_has_tip {
    static const bool value = has_tip<typename std::tuple_element<n, std::tuple<Ts...>>::type>::value;
};

// Generic iteration
template<size_t index, size_t n>
struct TupleIter
{
  template< typename... Ts >
  typename std::enable_if< nth_type_has_tip<index, Ts...>::value , void >::type
  static Iter(const std::tuple<Ts...>& tpl) 
  {
    std::cout << "tip\n";
    TupleIter<index + 1, n>::Iter(tpl );
  }

  template< typename... Ts >
  typename std::enable_if< !nth_type_has_tip<index, Ts...>::value , void >::type
  static Iter(const std::tuple<Ts...>& tpl) {
    std::cout << "no tip\n";
    TupleIter<index + 1, n>::Iter(tpl );
  }
};

// Base class, we've reached the tuple end
template<size_t n>
struct TupleIter<n, n>
{
  template<typename... Ts >
  static void Iter( const std::tuple<Ts...>& tpl ) {
    std::cout << "end\n";
  }
};

// Helper function that forwards the first call to TupleIter<>::Iter
template<typename... Ts>
void iterate(const std::tuple<Ts...> &tup) {
    TupleIter<0, sizeof...(Ts)>::Iter(tup);
}

int main() {
    A a;
    B b;
    Z z;
    std::tuple<const A&,const B&,const Z&> tup(a,b,z);
    iterate(tup);
}

Upvotes: 4

Related Questions