【问题标题】:compile time loops编译时循环
【发布时间】:2011-07-29 12:16:32
【问题描述】:

我想知道是否有可能有某种编译时循环。
例如,我有以下模板类:

template<class C, int T=10, int B=10>
class CountSketch
{
public:
    CountSketch()
    {   
         hashfuncs[0] = &CountSketch<C>::hash<0>;
         hashfuncs[1] = &CountSketch<C>::hash<1>;
         // ... for all i until i==T which is known at compile time
    };
private:
    template<int offset>
    size_t hash(C &c)
    {
        return (reinterpret_cast<int>(&c)+offset)%B;
    }
    size_t (CountSketch::*hashfuncs[T])(C &c);
};

因此我想知道是否可以使用循环执行循环来初始化 T 哈希函数。循环的边界在编译时是已知的,因此,原则上,我看不出它有任何无法完成的原因(特别是因为如果我手动展开循环它可以工作)。

当然,在这个特定的示例中,我可以只创建一个带有 2 个参数的哈希函数(尽管我猜它的效率会更低)。因此,我对解决这个特定问题不感兴趣,而是想知道类似情况下是否存在“编译时循环”。

谢谢!

【问题讨论】:

  • 我认为这不是一个好的答案,所以我只是在这里评论一下:可能编译器能够看到这些东西并对其进行优化——但我绝对不是编译器方面的专家。跨度>
  • 在发帖之前,我试着放了一个循环,但它没有用 - 因此我的问题;)
  • 你总是可以部分专门化你的模板,但这也不是一个好的解决方案:D
  • 这是一个不寻常的程序,需要对初始化阶段进行优化。
  • @Chris:是什么让你觉得这个类只能在程序的初始化阶段使用?

标签: c++ templates


【解决方案1】:

不,这不是直接可能的。模板元编程是一种纯函数式语言。通过它定义的每个值或类型都是不可变的。循环本质上需要可变变量(重复测试某些条件直到 X 发生,然后退出循环)。

相反,您通常会依赖递归。 (每次都用不同的模板参数实例化这个模板,直到达到某个终止条件)。

但是,这可以解决所有与循环相同的问题。

编辑:这是一个简单的示例,在编译时使用递归计算 N 的阶乘:

template <int N>
struct fac {
  enum { value = N * fac<N-1>::value };
};

template <>
struct fac<0> {
  enum { value = 1 };
};

int main() {
  assert(fac<4>::value == 24);
}

C++ 中的模板元编程是图灵完备的语言,所以只要不遇到各种内部编译器限制,基本上可以解决任何问题。

但是,出于实际目的,可能值得研究 Boost.MPL 等库,其中包含大量数据结构和算法,可简化大量元编程任务。

【讨论】:

  • 谢谢!你有一个如何实现这样一个递归模板的例子吗?
  • 刚刚添加了一个简单的(未经测试的)示例。请注意,将其用于实际目的可能会稍微复杂一些。 :)
  • 不尊重您的回答;我认为这是可能的。你可以看到我的答案。
  • 我不知道为什么这会不尊重我的回答。我们都说他的问题可以在编译时解决,不是吗? ;)
【解决方案2】:

是的。可以使用编译时递归

我正在尝试使用您的代码,但由于它不可编译,因此这里是一个修改和编译的示例:

template<class C, int T=10>
class CountSketch
{
  template<int N>
  void Init ()
  {
    Init<N-1>();
    hashfuncs[N] = &CountSketch<C>::template hash<N>;
    cout<<"Initializing "<<N<<"th element\n";
  }

public:
    CountSketch()
    {
      Init<T>();
    }
private:
   template<int offset>
   size_t hash(C &c)
   {
     return 0;
   }
   size_t (CountSketch::*hashfuncs[T])(C &c);
};

template<>
template<>
void CountSketch<int,10>::Init<0> ()
{
  hashfuncs[0] = &CountSketch<int,10>::hash<0>;
  cout<<"Initializing "<<0<<"th element\n";
}

Demo。此解决方案的唯一限制是您必须提供最终的专用版本,CountSketch&lt;int,10&gt;::Init&lt;0&gt;,适用于任何类型和大小。

【讨论】:

    【解决方案3】:

    您需要boost::mpl::for_eachboost::mpl::range_c 的组合。

    注意:这将产生运行时代码,而这正是您真正需要的。因为在编译时无法知道operator&amp; 的结果。至少我不知道。

    这样做的实际困难是构建一个以 int 参数(在我们的例子中为 mpl::int_)为模板的结构,并在调用 operator() 时执行分配,我们还需要一个函子来实际捕获这个指针。

    这比我预期的要复杂一些,但很有趣。

    #include <boost/mpl/range_c.hpp>
    #include <boost/mpl/vector.hpp>
    #include <boost/mpl/for_each.hpp>
    #include <boost/mpl/transform.hpp>
    #include <boost/mpl/copy.hpp>
    
    // aforementioned struct
    template<class C, class I>
    struct assign_hash;
    
    // this actually evaluates the functor and captures the this pointer
    // T is the argument for the functor U
    template<typename T>
    struct my_apply {
      T* t;
      template<typename U>
      void operator()(U u) {
        u(t);
      }
    };
    
    template<class C, int T=10, int B=10>
    class CountSketch
    {
    public:
      CountSketch()
        {   
          using namespace boost::mpl;
    
          // we need to do this because range_c is not an ExtensibleSequence
          typedef typename copy< range_c<int, 0, T>,
                                 back_inserter< vector<> > >::type r;
          // fiddle together a vector of the correct types
          typedef typename transform<r, typename lambda< assign_hash<C, _1 > >::type >
            ::type assignees;
    
          // now we need to unfold the type list into a run-time construct
          // capture this
          my_apply< CountSketch<C, T, B> > apply = { this };
          // this is a compile-time loop which actually does something at run-time
          for_each<assignees>(apply);
        };
    
      // no way around
      template<typename TT, typename I>
      friend struct assign_hash;
    
    private:
      template<int offset>
      size_t hash(C& c)
        {
          return c;
          // return (reinterpret_cast<int>(&c)+offset)%B;
        }
      size_t (CountSketch::*hashfuncs[T])(C &c);
    };
    
    // mpl uses int_ so we don't use a non-type template parameter 
    // but get a compile time value through the value member
    template<class C, class I>
    struct assign_hash {
      template<typename T>
      void operator()(T* t) {
        t->hashfuncs[I::value] = &CountSketch<C>::template hash<I::value>;
      }
    };
    
    int main() 
    {
      CountSketch<int> a;
    }
    

    【讨论】:

    • 这很难说,但它看起来给你一个运行时间循环,而不是一个编译时间循环。
    • @Gabe:这绝对是编译时。毕竟,“mpl”代表“元编程库”。
    • @sbi:不。这是一个运行时循环。 for_each 如果导致编译时循环,将或多或少一文不值(或至少不值一提)。实时调频。很明显,Boost 的 for_each 和 c++0x 的 for_each 都是运行时构造。
    • @David: mpl::for_each 和 std::for_each 根本不同。一个处理一系列类型,另一个处理一系列值。在任何情况下,类型序列都是编译时构造。 mpl::for_each 是运行时的意思是它实际上会导致在运行时执行的代码。实际的迭代(实际上是递归)将是一个编译时构造。
    • 我添加了实际的实现。也许其他人可以使它更简洁一些,因为它现在非常冗长,而且事物的名称很烂。
    【解决方案4】:

    使用 C++20 和 consteval 编译时循环成为可能,无需执行模板地狱,除非该值可以具有多种类型:

    consteval int func() {
        int out = 0;
        for(int i = 10; i--;) out += i;
        return out;
    }
    int main() {
        std::cout << func(); // outputs 45
    }
    

    【讨论】:

    【解决方案5】:

    有些编译器会看到循环并展开它。但它不是必须完成的语言规范的一部分(事实上,语言规范在这样做的方式上抛出了各种障碍),并且不能保证它会在特定情况下完成,即使在“知道如何”的编译器上。

    有一些语言明确地做到了这一点,但它们是高度专业化的。

    (顺便说一句,不能保证初始化的“展开”版本会在“编译时”以相当有效的方式完成。但大多数编译器在不编译到调试目标时会这样做。)

    【讨论】:

    • 循环展开被认为是 C++ 编译器中的常见优化。
    • 确实 - 但如果我执行循环,我只会遇到编译时错误(使用 VS 2008)。
    • 是的,“常见”,但远不能保证。而且通常“展开”只包括端到端放置 2-4 次迭代,以简单地节省一些寻址计算。
    • @WhitAngl -- 我猜 jalf 有你的问题的“答案”。我没有过多关注模板方面。 (模板让我头疼。)
    • 我很想知道那些“障碍”是什么。请提供一些例子好吗?
    【解决方案6】:

    我认为,这是上面给出的解决方案的更好版本。
    您可以看到我们在函数 params 上使用了编译时递归。​​
    这样可以将所有逻辑放入您的类中,并且 Init(int_) 的基本情况非​​常清楚 - 什么都不做 :)
    只是这样您就不会担心性能损失,知道优化器会丢弃这些未使用的参数。
    事实上,所有这些函数调用无论如何都会被内联。这就是重点。

    #include <string.h>
    #include <stdio.h>
    #include <algorithm>
    #include <iostream>
    
    using namespace std;
    
    template <class C, int N = 10, int B = 10>
    class CountSketch {
     public:
      CountSketch() {
        memset(&_hashFunctions, sizeof(_hashFunctions), 0); // for safety
        Init(int_<N>());
      }
    
      size_t HashAll(C& c)
      {
          size_t v = 0;
          for(const auto& h : _hashFunctions) 
          {
            v += (this->*h)(c); // call through member pointer
          }
          return v;
      }
    
     private:
        template<int offset>
        size_t hash(C &c)
        {
            return (reinterpret_cast<size_t>(&c)+offset)%B;
        }
    
      size_t (CountSketch::*_hashFunctions[N])(C &c);
    
     private: // implementation detail
    
      // Notice: better approach.
      // use parameters for compile-time recursive call.
      // you can just override for the base case, as seen for N-1 below
      template <int M>
      struct int_ {};
    
      template <int M>
      void Init(int_<M>) {
        Init(int_<M - 1>());
        _hashFunctions[M - 1] = &CountSketch<C, N, B>::template hash<M>;
        printf("Initializing %dth element\n", M - 1);
      }
    
      void Init(int_<0>) {}
    };
    
    int main() {
      int c;
      CountSketch<int, 10> cs;
    
      int i;
      cin >> i;
    
      printf("HashAll: %d", cs.HashAll(c));
      return 0;
    }
    

    Compiler Explorer

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-03
      • 2021-10-04
      • 1970-01-01
      • 1970-01-01
      • 2015-09-28
      • 2013-04-03
      相关资源
      最近更新 更多