fra*_*sco 14 c++ template-meta-programming variadic-templates c++17 c++20
在某些情况下,for在编译时评估/展开循环可能是有用/必要的。例如,要遍历a的元素tuple,需要使用std::get<I>,这取决于模板int参数I,因此必须在编译时对其进行评估。使用编译递归可以解决一个特定的问题,例如此处,此处以及std::tuple 此处专门讨论的问题。
但是,我对如何实现通用编译时for循环感兴趣。
以下c++17代码实现了这个想法
#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;
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
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)
Run Code Online (Sandbox Code Playgroud)
The questions are:
compile_time_for such that it accepts a template function as its first argument?OperatorType<start> at every loop iteration?c++20?
- 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.
- 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.
- 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;
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
-- 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>{}
Run Code Online (Sandbox Code Playgroud)
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)...);
}
Run Code Online (Sandbox Code Playgroud)
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)...); }
Run Code Online (Sandbox Code Playgroud)
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 */ }
Run Code Online (Sandbox Code Playgroud)