在某些情况下,在编译时评估/展开for
循环可能是有用/必要的。例如,要迭代tuple
的元素,需要使用std::get<I>
,这取决于模板int
参数I
,因此必须在编译时进行评估。使用编译递归可以解决特定问题,例如讨论here,here,特别是std::tuple
here。
但是,我对如何实现通用编译时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;
}
虽然代码有效,但是能够简单地向例程compile_time_for
提供模板函数,而不是在每次迭代时实例化的模板类会更好。
但是,如下所示的代码无法在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;
}
使用gcc 7.3.0和选项std=c++17
,第一个错误是
for2.cpp:7:25: error: ‘auto’ parameter not permitted in this context
void compile_time_for(F f, Args... args)
问题是:
compile_time_for
这样它接受模板函数作为它的第一个参数?OperatorType<start>
类型的对象?c++20
中引入类似编译时for循环的功能?
- 有没有办法编写compile_time_for,以便它接受模板函数作为它的第一个参数?
简答:不。
答案很长:模板函数不是一个对象,是一个对象的集合,你可以传递给一个函数,一个参数,一个对象,一个非对象的集合。
这类问题的通常解决方案是将模板函数包装在类中并传递类的对象(或者只是类型,如果函数被包装为静态方法)。这正是您在工作代码中采用的解决方案。
- 如果问题1是肯定的,那么第一个工作代码中是否存在开销,因为例程在每次循环迭代时都会创建一个类型为OperatorType的对象?
问题1是否定的。
- 是否有计划在即将推出的c ++ 20中引入类似编译时for循环的功能?
我不知道C ++ 20是否足以回答这个问题,但我想不会传递一组函数。
无论如何,你可以使用从C ++ 14开始的std::make_index_sequence
/ std::index_sequence
进行一种编译循环。
例如,如果您接受在myprint()
函数之外提取touple值,则可以将其包装在lambda中并按如下方式编写内容(使用C ++ 17模板折叠;在C ++ 14中稍微复杂一点)
#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;
}
如果你真的想要在函数内部提取元组元素(或元组元素),我能想象的最好的是将你的第一个例子变换如下
#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;
}
- 编辑 -
OP问道
使用index_sequence比我的第一个代码有一些优势吗?
我不是专家,但这样可以避免递归。从模板的角度来看,编译器具有递归限制,可以是严格的。这样你就可以避免它们。
此外,如果设置模板参数
end > start
,则代码无法编译。 (可以想象一种情况,您希望编译器确定是否实例化了一个循环)
我想你的意思是我的代码不能编译,如果start > end
。
不好的部分是没有检查这个问题所以编译器尝试编译我的代码也是在这种情况下;所以遇到
std::make_index_sequence<end-start>{}
其中end - start
是一个负数,但是由期望无符号数的模板使用。所以end - start
成为一个非常伟大的正数,这可能会导致问题。
你可以避免在static_assert()
内强加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)...);
}
或者也许你可以使用SFINAE来禁用该功能
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)...); }
如果你愿意,使用SFINAE你可以添加一个重载的compile_time_for()
版本来管理end < start
案例
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 */ }
我将回答有关如何修复上一个代码示例的问题。
它不编译的原因在于:
template <int start, int end, template <int, typename...> class F, typename... Args>
void compile_time_for(F f, Args... args)
/\
F是一个模板,如果没有替换模板参数,则不能拥有模板类的对象。例如。你不能拥有std::vector
类型的对象,但可以有std::vector<int>
的对象。我建议你用模板运算符()制作F
仿函数:
#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;
}