编译时间循环

时间:2011-07-29 12:16:32

标签: c++ templates

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

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个参数创建一个哈希函数(虽然我猜它的效率会低一些)。因此我对解决这个特定问题不感兴趣,而是知道类似情况下是否存在“编译时循环”。

谢谢!

6 个答案:

答案 0 :(得分:25)

不,这不是直接可能的。模板元编程是一种纯函数式语言。通过它定义的每个值或类型都是不可变的。一个循环固有地需要可变变量(重复测试一些条件直到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这样的库,它包含大量的数据结构和算法,可以简化许多元编程任务。

答案 1 :(得分:22)

是。可以使用编译时递归

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

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<int,10>::Init<0>,无论何种类型和大小。

答案 2 :(得分:4)

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

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

实际的难点是构建一个在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;
}

答案 3 :(得分:1)

有些编译器会看到循环并展开它。但它不是必须完成的语言规范的一部分(实际上,语言规范在执行它时会引发各种障碍),并且无法保证在特定情况下会完成它,甚至在“知道如何”的编译器上。

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

(顺便说一句,无法保证初始化的“展开”版本将在“编译时”以合理有效的方式完成。但是大多数编译器在未编译到调试目标时都会这样做。)

答案 4 :(得分:1)

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

#include <iostream>
using namespace std;

template <class C, int N = 10>
class CountSketch {
 public:
  CountSketch() {
    memset(&hashfuncs, sizeof(hashfuncs), 0); // for safety
    // Notice: we start with (N-1) since we need a zero based index
    Init(int_<N - 1>());
  }

 private:
  template <int offset>
  size_t hash(C &c) {
    return 0;
  }

  size_t (CountSketch::*hashfuncs[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 N>
  struct int_ {};

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

  void Init(int_<-1>)  // Notice: this gives you the N=0 case for free!
  {}
};

int main() {
  CountSketch<int, 10> cs;
  return 0;
}

答案 5 :(得分:1)

使用 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
}
相关问题