C++通用的编译时循环

16
在某些情况下,有一个在编译时评估/展开的for循环可能是有用/必要的。例如,要迭代tuple的元素,需要使用std::get<I>,它依赖于模板int参数I,因此必须在编译时进行评估。 使用编译递归可以解决特定问题,例如在这里这里和特别针对std::tuple这里中讨论的问题。
然而,我对如何实现通用的编译时for循环感兴趣。
以下代码实现了这个想法。
#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提供一个模板函数,而不是在每次迭代中实例化一个模板类。
然而,像下面这样的代码在中无法编译。
#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
使用 std::index_sequencestd::make_index_sequence 怎么样? - max66
或者使用std::applystd::apply([](const auto&...args) { ((std::cout << args << " "), ...); }, x); - Jarod42
@Jarod42 如果每次循环迭代要执行的操作取决于迭代本身(例如通过模板参数),那么std::apply将无法完成任务。 - francesco
@francesco:你可以使用(冗长的?)decltype(args)来定义类型。C++20也允许在lambda中使用显式模板。 - Jarod42
我认为有一个关于可变参数for循环的提案,类似于for...(/*..*/)。但是我没有成功找到它。 - Jarod42
1
对于您的第三个问题,P1306(扩展语句) 是提议中的编译时循环。它仍在可能包含在C++20中的轨道上,但不能保证。 - Rémi Galan Alfonso
2个回答

7
有没有一种方法编写compile_time_for,使其接受模板函数作为其第一个参数?
简短回答:没有。
长答案:模板函数不是对象,它是对象的集合,您可以将对象作为参数传递给函数,而不是对象的集合。
这种类型问题的常见解决方案是将模板函数包装在类中,并传递类的对象(或者如果函数被包装为静态方法,则仅传递类型)。这正是您在工作代码中采用的解决方案。
如果问题1的答案是肯定的,那么由于在每个循环迭代中创建OperatorType类型的对象,第一个工作代码是否存在开销?
问题1的答案是否定的。
有计划引入像编译时for循环这样的功能在即将推出的c++20中吗?
我不了解C ++ 20足以回答这个问题,但我认为不会通过传递一组函数来实现。
无论如何,您可以使用std :: make_index_sequence / std :: index_sequence从C ++ 14开始执行某种编译时for循环。
例如,如果您愿意在 myprint()函数外提取元组值,您可以将其包装在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;
}

-- 编辑 --

楼主提问:

使用 index_sequence 比我第一种代码有什么优势吗?

我不是专家,但这样可以避免递归。从模板的角度来看,编译器有可能会施加严格的递归限制,而使用 index_sequence 可以避免这种情况。

此外,如果将模板参数设置为 end > start,则您的代码无法编译。 (可以想象一种情况,您想让编译器确定是否实例化循环)

我想你指的是我的代码无法编译,如果 start > end

不好的一点是没有关于这个问题的检查,因此编译器会尝试在这种情况下编译我的代码;所以会出错。

 std::make_index_sequence<end-start>{}

end - start 是一个负数,但被期望为无符号数的模板使用时,就会出现问题。因此,end - start 变成了一个非常大的正数,这可能会导致问题。

您可以通过在 compile_time_for() 中施加一个 static_assert() 来避免这个问题。

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 */ }

非常感谢您的回答。第二个例子更适合我的问题,因为我可以想象一个for循环,在每次迭代中完成的任务取决于迭代本身。使用index_sequence是否有什么优点,胜过我的第一个代码?此外,如果设置模板参数end > start,您的代码将无法编译。(有一种情况是您希望编译器确定循环是否被实例化) - francesco
@francesco - 回答得到了改进,希望这能有所帮助。 - max66

3

我将回答如何修复您的最后一段代码。

它无法编译的原因在于这里:

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>的对象。我建议你使用一个带有模板operator()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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接