c++ generic compile-time for loop
Asked Answered
S

2

17

In some contexts, it could be useful/necessary to have a for loop evaluated/unrolled at compile time. For example, to iterate over the elements of a tuple, one needs to use std::get<I>, which depends on a template int parameter I, hence it has to be evaluated at compile time. Using compile recursion one can solve a specific problem, as for instance discussed here, here, and, specifically for std::tuple here.

I am interested, however, on how to implement a generic compile-time for loop.

The following c++17 code implements this idea

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

template <int start, int end, template <int> class OperatorType, typename... Args>
void compile_time_for(Args... args)
{
  if constexpr (start < end)
         {
           OperatorType<start>()(std::forward<Args>(args)...);
           compile_time_for<start + 1, end, OperatorType>(std::forward<Args>(args)...);
         }    
}

template <int I>
struct print_tuple_i {
  template <typename... U>
  void operator()(const std::tuple<U...>& x) { std::cout << std::get<I>(x) << " "; }
};

int main()
{
  std::tuple<int, int, std::string> x{1, 2, "hello"};

  compile_time_for<0, 3, print_tuple_i>(x);

  return 0;
}

While the code works, it would be nicer to be able to simply provide a template function to the routine compile_time_for, rather than a template class to be instantiated at each iteration.

A code like the following, however, does not compile in c++17

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

template <int start, int end, template <int, typename...> class F, typename... Args>
void compile_time_for(F f, Args... args)
{
  if constexpr (start < end)
         {
           f<start>(std::forward<Args>(args)...);
           compile_time_for<start + 1, end>(f, std::forward<Args>(args)...);
         }    
}

template <int I, typename... U>
void myprint(const std::tuple<U...>& x) { std::cout << std::get<I>(x) << " "; }

int main()
{
  std::tuple<int, int, std::string> x{1, 2, "hello"};

  compile_time_for<0, 3>(myprint, x);

  return 0;
}

With gcc 7.3.0 and option std=c++17 the first error is

for2.cpp:7:25: error: ‘auto’ parameter not permitted in this context
 void compile_time_for(F f, Args... args)

The questions are:

  1. Is there a way to write compile_time_for such that it accepts a template function as its first argument?
  2. If question 1. is positive, is there an overhead in the first working code, due to the fact that the routine create an object of type OperatorType<start> at every loop iteration?
  3. Are there plans to introduce a feature like a compile-time for loop in the upcoming c++20?
Sellars answered 12/4, 2019 at 9:20 Comment(6)
what about using std::index_sequence and std::make_index_sequence?Energetic
or std::apply: std::apply([](const auto&...args) { ((std::cout << args << " "), ...); }, x);.Everetteverette
@Everetteverette std::appy will not do the job if the operation to be done at each loop iteration depends on the iteration itself (via a template parameter, for instance)Sellars
@francesco: You can have type with (verbose?) decltype(args). C++20 would allow to have explicit template in lambda too, normally.Everetteverette
I think there are a proposal for variadic for, something like for...(/*..*/). But I don't succeed to find it.Everetteverette
For your 3), P1306 (expansion statements) is the proposed compile time for loop. It is still on track for possible inclusion in C++20 but not guaranteed.Wealthy
E
8
  1. Is there a way to write compile_time_for such that it accepts a template function as its first argument?

Short answer: no.

Long answer: a template function isn't an object, is a collection of objects and you can pass to a function, as an argument, an object, non a collection of objects.

The usual solution to this type of problem is wrap the template function inside a class and pass an object of the class (or simply the type, if the function is wrapped as a static method). That is exactly the solution you have adopted in your working code.

  1. If question 1. is positive, is there an overhead in the first working code, due to the fact that the routine create an object of type OperatorType at every loop iteration?

Question 1 is negative.

  1. Are there plans to introduce a feature like a compile-time for loop in the upcoming c++20?

I don't know C++20 enough to respond this question but I suppose not passing a set of function.

Anyway, you can do a sort of compile-time for loop using std::make_index_sequence/std::index_sequence starting from C++14.

By example, if you accept to extract the touple value outside your myprint() function, you can wrap it inside a lambda and write something as follows (using also C++17 template folding; in C++14 is a little more complicated)

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

template <typename T>
void myprint (T const & t)
 { std::cout << t << " "; }

template <std::size_t start, std::size_t ... Is, typename F, typename ... Ts>
void ctf_helper (std::index_sequence<Is...>, F f, std::tuple<Ts...> const & t)
 { (f(std::get<start + Is>(t)), ...); }

template <std::size_t start, std::size_t end, typename F, typename ... Ts>
void compile_time_for (F f, std::tuple<Ts...> const & t)
 { ctf_helper<start>(std::make_index_sequence<end-start>{}, f, t); }

int main()
{
  std::tuple<int, int, std::string> x{1, 2, "hello"};

  compile_time_for<0, 3>([](auto const & v){ myprint(v); }, x);

  return 0;
}

If you really want extract the tuple element (or tuples elements) inside the function, the best I can imagine is transform your first example as follows

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

template <std::size_t start, template <std::size_t> class OT,
          std::size_t ... Is, typename... Args>
void ctf_helper (std::index_sequence<Is...> const &, Args && ... args)
 { (OT<start+Is>{}(std::forward<Args>(args)...), ...); }

template <std::size_t start, std::size_t end,
          template <std::size_t> class OT, typename... Args>
void compile_time_for (Args && ... args)
 { ctf_helper<start, OT>(std::make_index_sequence<end-start>{},
                         std::forward<Args>(args)...); }

template <std::size_t I>
struct print_tuple_i
 {
   template <typename ... U>
   void operator() (std::tuple<U...> const & x)
    { std::cout << std::get<I>(x) << " "; }
 };

int main()
{
  std::tuple<int, int, std::string> x{1, 2, "hello"};

  compile_time_for<0u, 3u, print_tuple_i>(x);

  return 0;
}

-- EDIT --

The OP asks

Is there some advantage of using index_sequence over my first code?

I'm not an expert but this way you avoid recursion. Compilers have recursion limits, from the template point of view, that can be strict. This way you avoid they.

Also, your code does not compile if you set the template parameters end > start. (One can imagine a situation where you want the compiler to determine if a loop is instantiated at all)

I suppose you mean that my code does not compile if start > end.

The bad part is that there aren't check about this problem so the compiler try to compile my code also in this case; so encounter

 std::make_index_sequence<end-start>{}

where end - start is a negative number but used by a template that expect an unsigned number. So end - start become a very great positive number and this can cause problems.

You can avoid this problem imposing a static_assert() inside compile_time_for()

template <std::size_t start, std::size_t end,
          template <std::size_t> class OT, typename... Args>
void compile_time_for (Args && ... args)
 { 
   static_assert( end >= start, "start is bigger than end");

   ctf_helper<start, OT>(std::make_index_sequence<end-start>{},
                         std::forward<Args>(args)...);
 }

Or maybe you can use SFINAE to disable the function

template <std::size_t start, std::size_t end,
          template <std::size_t> class OT, typename... Args>
std::enable_if_t<(start <= end)> compile_time_for (Args && ... args)
 { ctf_helper<start, OT>(std::make_index_sequence<end-start>{},
                         std::forward<Args>(args)...); }

If you want, using SFINAE you can add an overloaded compile_time_for() version to manage the end < start case

template <std::size_t start, std::size_t end,
          template <std::size_t> class OT, typename ... Args>
std::enable_if_t<(start > end)> compile_time_for (Args && ...)
 { /* manage the end < start case in some way */ }
Energetic answered 12/4, 2019 at 9:46 Comment(2)
Thanks a lot for the answer. The second example fits more my question, because I can imagine a for loop where the task done at each iteration depends on the iteration itself. Is there some advantage of using index_sequence over my first code? Also, your code does not compile if you set the template parameters end > start. (One can imagine a situation where you want the compiler to determine if a loop is instantiated at all)Sellars
@Sellars - answer improved; hope this helps.Energetic
B
4

I'll answer on the question how to fix your last code sample.

The reason why it doesn't compile is here:

template <int start, int end, template <int, typename...> class F, typename... Args>
void compile_time_for(F f, Args... args)
                      /\

F is a template, you can't have an object of a template class without template parameters being substituted. E.g. you can't have on object of std::vector type, but can have object of std::vector<int>. I suggest you to make F functor with a template operator() :

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

template <int start, int end, typename F, typename... Args>
void compile_time_for(F f, Args... args)
{
  if constexpr (start < end)
         {
           f.template operator()<start>(std::forward<Args>(args)...);
           compile_time_for<start + 1, end>(f, std::forward<Args>(args)...);
         }    
}

struct myprint
{
    template <int I, typename... U>
    void operator()(const std::tuple<U...>& x) { std::cout << std::get<I>(x) << " "; }
};

int main()
{
  std::tuple<int, int, std::string> x{1, 2, "hello"};

  compile_time_for<0, 3>(myprint(), x);

  return 0;
}
Bs answered 12/4, 2019 at 9:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.