C++ 通用编译时循环

c++ generic compile-time for loop

本文关键字:循环 编译 C++      更新时间:2023-10-16

在某些情况下,在编译时评估/展开for循环可能是有用/必要的。例如,要迭代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;
}

虽然代码有效,但最好能够简单地为例程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)

问题是:

  1. 有没有办法编写compile_time_for,使其接受模板函数作为其第一个参数?
  2. 如果问题 1. 是肯定的,那么由于例程在每次循环迭代中都会创建一个类型OperatorType<start>的对象,因此第一个工作代码中是否存在开销?
  3. 是否有计划在即将到来的c++20中引入像编译时 for 循环这样的功能?
  1. 有没有办法编写compile_time_for,使其接受模板函数作为其第一个参数?

简短的回答:不。

长答案:模板函数不是对象,是对象的集合,您可以传递给函数,作为参数,对象,而不是对象的集合。

此类问题的通常解决方案是将模板函数包装在类中并传递类的对象(或者如果函数包装为静态方法,则只是类型)。这正是您在工作代码中采用的解决方案。

  1. 如果问题 1. 是肯定的,那么由于例程在每次循环迭代中都会创建一个 OperatorType 类型的对象,因此第一个工作代码中是否有开销?

问题1是否定的。

  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;
}