C++ 泛型编译时 for 循环

c++ generic compile-time for loop

提问人:francesco 提问时间:4/12/2019 最后编辑:max66francesco 更新时间:4/13/2019 访问量:5532

问:

在某些情况下,在编译时评估/展开循环可能是有用/必要的。例如,要遍历 的元素,需要使用 ,它依赖于模板参数,因此必须在编译时对其进行计算。 使用编译递归可以解决一个特定的问题,例如这里讨论的,这里,特别是这里fortuplestd::get<I>intIstd::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. 是否有计划在即将到来的 for 循环中引入像编译时这样的功能?c++20
C++ 17 变量 模板元编程 C++20

评论

1赞 max66 4/12/2019
使用和 呢?std::index_sequencestd::make_index_sequence
0赞 Jarod42 4/12/2019
或:。std::applystd::apply([](const auto&...args) { ((std::cout << args << " "), ...); }, x);
0赞 francesco 4/12/2019
如果在每次循环迭代中要完成的操作取决于迭代本身(例如,通过模板参数),则@Jarod42将无法完成这项工作std::appy
0赞 Jarod42 4/12/2019
@francesco:你可以用(详细?)打字。通常,C++20 也允许在 lambda 中使用显式模板。decltype(args)
1赞 Rémi Galan Alfonso 4/15/2019
对于 3),P1306(扩展语句)是循环的建议编译时间。它仍然有望被纳入 C++20,但不能保证。

答:

3赞 Dmitry Gordon 4/12/2019 #1

我将回答如何修复最后一个代码示例的问题。

它不编译的原因在这里:

template <int start, int end, template <int, typename...> class F, typename... Args>
void compile_time_for(F f, Args... args)
                      /\

F 是一个模板,如果不替换模板参数,就不能有模板类的对象。例如,你不能有 on 类型的对象,但可以有 .我建议你用模板运算符()做函子:std::vectorstd::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;
}
7赞 max66 4/12/2019 #2
  1. 有没有办法编写compile_time_for以便它接受模板函数作为其第一个参数?

简短的回答:不。

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

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

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

问题 1 是否定的。

  1. 是否有计划在即将到来的 c++20 中引入类似编译时 for 循环的功能?

我对 C++20 的了解不足以回答这个问题,但我想没有传递一组函数。

无论如何,你可以使用 / 从 C++14 开始进行一种编译时循环。std::make_index_sequencestd::index_sequence

举个例子,如果你接受在函数之外提取 touple 值,你可以把它包装在一个 lambda 中,并编写如下内容(也使用 C++17 模板折叠;在 C++14 中稍微复杂一些)myprint()

#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>{}

where 是一个负数,但由需要无符号数字的模板使用。所以成为一个非常大的正数,这可能会导致问题。end - startend - 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 */ }

评论

0赞 francesco 4/13/2019
非常感谢您的回答。第二个例子更适合我的问题,因为我可以想象一个循环,其中每次迭代完成的任务取决于迭代本身。使用我的第一个代码有什么优势吗?此外,如果设置模板参数,则代码不会编译。(可以想象这样一种情况,你希望编译器确定一个循环是否被实例化)forindex_sequenceend > start
0赞 max66 4/13/2019
@francesco - 答案得到改善;希望这会有所帮助。