if constexpr with recursive parameter packs

Viewed 960

I am trying to recursively run through a typelist so I can do some run-time code based on each type in the list. I would like to be able to recursively run through all the types in a tuple in a function in a struct (not in a function in the struct) without using "if constexpr" to terminate the recursion.

I have a snippet of code the shows the recursion working with constexpr.

#include <iostream>
#include <string>
#include <tuple>
template <typename ...Ts>
struct temp{
    using TypeList = std::tuple<Ts...>;
    constexpr static std::size_t _N = std::tuple_size<TypeList>::value;

    void print_this()
    { 
        _inner_print<_N,_N>();
    }

    template <std::size_t N, std::size_t MAX>
    void _inner_print()
    {
        if constexpr ( N != 0 )
        {
            std::cout << "Call #"<<MAX-N<<std::endl;
            ////////////////////////
            /* other dynamic code */
            ////////////////////////
            _inner_print<N-1, MAX>();
        }
    }

    TypeList _mem;
};

int main()
{
    std::string name;
    temp<int, int, int> t1;
    t1.print_this();
}

I would like to be able to do the same recursion with C++14, instead of C++17 w/ "if constexpr".

Thank you!

4 Answers

The trick is to use index_sequence.

Here is a C++14 working solution, improved using @MartinMorterol suggestion.

// -*- compile-command: "g++ -Wall -std=c++14 poub.cpp; ./a.out"; -*-
#include <iostream>
#include <string>
#include <tuple>
#include <type_traits>

template <typename... Ts>
struct temp
{
  using TypeList = std::tuple<Ts...>;
  constexpr static std::size_t _N = std::tuple_size<TypeList>::value;

  void print_this() { _inner_print(std::make_index_sequence<_N>()); }

  template <std::size_t... IDX>
  void _inner_print(std::index_sequence<IDX...>)
  {
    auto dummy = {0, (_inner_print<IDX>(),0)...};
    (void)dummy;
  }

  template <std::size_t IDX>
  void _inner_print()
  {
    std::cout << "\nCall #" << IDX 
              << " sizeof " << sizeof(std::get<IDX>(_mem));
  }

  TypeList _mem;
};

int main()
{
  std::string name;
  temp<int, double, char> t1;
  t1.print_this();
}

which prints:

g++ -Wall -std=c++14 poub.cpp; ./a.out

Call #0 sizeof 4
Call #1 sizeof 8
Call #2 sizeof 1

My initial answer (using recursion)

// -*- compile-command: "g++ -std=c++14 poub.cpp; ./a.out"; -*-
#include <iostream>
#include <string>
#include <tuple>
#include <type_traits>

template <typename... Ts>
struct temp
{
  using TypeList = std::tuple<Ts...>;
  constexpr static std::size_t _N = std::tuple_size<TypeList>::value;

  void print_this() { _inner_print(std::make_index_sequence<_N>()); }

  template <std::size_t... IDX>
  void _inner_print(std::index_sequence<IDX...>)
  {
    _inner_print(std::integral_constant<std::size_t, IDX>()...);
  }

  template <std::size_t HEAD_IDX, typename... TAIL>
  void _inner_print(std::integral_constant<std::size_t, HEAD_IDX>, TAIL... tail)
  {
    std::cout << "\nCall #" << HEAD_IDX 
              << " sizeof " << sizeof(std::get<HEAD_IDX>(_mem));

    // whatever you want HERE ...

    _inner_print(tail...);
  }
  void _inner_print(){};

  TypeList _mem;
};

int main()
{
  std::string name;
  temp<int, double, char> t1;
  t1.print_this();
}

If you can change your _inner_print function to a class, you can make use of a partial specialization to end the recursion:

template <std::size_t N, std::size_t MAX>
struct _inner_print
{
    _inner_print()
    {
        std::cout << "Call #"<<MAX-N<<std::endl;
        ////////////////////////
        /* other dynamic code */
        ////////////////////////
        _inner_print<N-1, MAX>();
    }
};

template <std::size_t MAX> struct _inner_print<0, MAX> { };

Rather than calling _inner_print() as a function, it becomes a declaration for an unnamed temporary, invoking the constructor that performs your output.

You should use partial specialization. But you can't do this with a function.

You should use a struct to do the trick.

#include <iostream>
#include <string>
#include <tuple>

template <std::size_t N, std::size_t MAX, class T>
struct inner_print_impl{
        static void run(const T&  caller)
        {

            std::cout << "Call #"<<MAX-N<<  " " << caller.a << std::endl;
            ////////////////////////
            /* other dynamic code */
            ////////////////////////
            inner_print_impl<N-1, MAX , T>::run(caller);
        }
 };

template < std::size_t MAX, class T>
struct inner_print_impl<0, MAX ,  T>{
        static void run(const T&  caller)
        {

            std::cout << "Call #"<<MAX<<  " " << caller.a << std::endl;
            ////////////////////////
            /* other dynamic code */
            ////////////////////////

            // no recursion
        }
 };


template <typename ...Ts>
struct temp{

    using TypeList = std::tuple<Ts...>;
    constexpr static std::size_t N_ = std::tuple_size<TypeList>::value;

    template <std::size_t N, std::size_t MAX, class T>
    friend struct inner_print_impl;
    void print_this()
    { 
        inner_print_impl<N_,N_, temp<Ts...> >::run(*this);
    }

    TypeList _mem;

    private : 

        int a ; // test acces
};

int main()
{
    std::string name;
    temp<int, int, int> t1;
    t1.print_this();
}

Note :

  1. If you need to acces to a private membre, you will need to pass *this to the call and add the new struct as friend of your class
  2. In my code, I have a duplication on the /* other dynamic code */ part. You may :
    • Use a function
    • make your template parameter a int instead of size_t and stop at -1 rather than 0

PS :

I don't get the part

in a tuple in a function in a struct (not in a function in the struct)

I hope I didn't miss something

Edit :

My code do one more iteration than your, you may just empty this :

template < std::size_t MAX, class T>
struct inner_print_impl<0, MAX ,  T>{
        static void run(const T&  caller)
        {
        }
 };

And you don't display for the 0 case.

template<class Integral, Integral N>
auto dispatch( std::integral_constant<Integral, N> ) {
  return [](auto&&...args){
    return std::get<N>( std::forward_as_tuple( decltype(args)(args)... ) );
  };
}
template<std::size_t N>
auto dispatch() {
  return dispatch( std::integral_constant<std::size_t, N>{} );
}

this little beauty gives you much of the magic of if constexpr in .

template <std::size_t N, std::size_t MAX>
void _inner_print() {
  dispatch( std::integral_constant<bool, N==0>{} )
  (
    // 0, aka false branch:
    [&](auto Nval){
      std::cout << "Call #"<<MAX-Nval<<std::endl;
      ////////////////////////
      /* other dynamic code */
      ////////////////////////
      _inner_print<Nval-1, MAX>();
    },
    // 1, aka true branch:
    [](auto&&){}
  )( std::integral_constant<std::size_t, N>{} );
}

Live example.

Note that we have to pass in the N via an argument to the lambda, because we want the validity of the lambda's body to vary based on its argument, which is only passed to the right one.


Alternatively, you can use an overload helper:

template<class T0, class...Ts>
struct overloaded;
template<class Lhs, class Rhs, class...Ts>
struct overloaded<Lhs, Rhs, Ts...>:
  overloaded<Lhs>, overloaded<Rhs,Ts...>
{
  using overloaded<Lhs>::operator();
  using overloaded<Rhs,Ts...>::operator();
  template<class A0, class...As>
  explicit overloaded(A0&&a0, As&&...as) :
    overloaded<Lhs>(std::forward<A0>(a0)),
    overloaded<Rhs,Ts...>( std::forward<As>(as)... )
  {}
  overloaded(overloaded const&)=default;
  overloaded(overloaded &&)=default;
  overloaded& operator=(overloaded const&)=default;
  overloaded& operator=(overloaded &&)=default;
};
template<class T0>
struct overloaded<T0> : T0 {
  using T0::operator();
  template<class A0>
  explicit overloaded(A0&&a0):
    T0(std::forward<A0>(a0))
  {}

  overloaded(overloaded const&)=default;
  overloaded(overloaded &&)=default;
  overloaded& operator=(overloaded const&)=default;
  overloaded& operator=(overloaded &&)=default;
};
template<class R, class...Args>
struct overloaded<R(*)(Args...)> {
  R operator()(Args... args) const {
    return pf(std::forward<Args>(args)...);
  }
  R(*pf)(Args...);
  overloaded( R(*f)(Args...) ):pf(f) {}

  overloaded(overloaded const&)=default;
  overloaded(overloaded &&)=default;
  overloaded& operator=(overloaded const&)=default;
  overloaded& operator=(overloaded &&)=default;
};
template<class...Args>
auto overload( Args&&...args ) {
  return overloaded<std::decay_t<Args>...>(
    std::forward<Args>(args)...
  );
}

Here is how you can use overloaded:

template <std::size_t N, std::size_t MAX>
void _inner_print()
{
  std::integral_constant<std::size_t, N> index;
  overload(
    [](std::integral_constant<std::size_t, 0>) {},
    [&](auto index) {
      std::cout << "Call #"<<MAX-index<<std::endl;
      ////////////////////////
      /* other dynamic code */
      ////////////////////////
      _inner_print<index-1, MAX>();
    }
  )(index);
}

note that the rule is that the only-sometimes-compiling dynamic code must depend on the auto arguments to the lambda.

Live example.

Related