C ++通用编译时循环

时间:2019-04-12 09:20:58

标签: c++ c++17 variadic-templates template-meta-programming c++20

在某些情况下,在编译时评估/展开for循环可能是有用/必要的。例如,要遍历tuple的元素,需要使用std::get<I>,它取决于模板int参数I,因此必须在以下位置进行评估编译时间。 使用编译递归可以解决一个特定的问题,例如herehere,尤其是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)

问题是:

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

2 个答案:

答案 0 :(得分:6)

  
      
  1. 是否可以编写compile_time_for使其接受模板函数作为其第一个参数?
  2.   

简短回答:否。

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

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

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

问题1是否定的。

  
      
  1. 是否计划在即将发布的c ++ 20中引入诸如编译时for循环之类的功能?
  2.   

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

如果您真的想提取函数中的tuple元素(或多个tuple元素),我能想到的最好的方法就是按如下所示变换您的第一个示例

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

答案 1 :(得分: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;
}