27

I would like to know if it is possible to have sort of compile time loops.
For example, I have the following templated class:

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);
};

I would thus like to know if I can do a loop to initialize the T hash functions using a loop. The bounds of the loops are known at compile time, so, in principle, I don't see any reason why it couldn't be done (especially since it works if I unroll the loop manually).

Of course, in this specific example, I could just have made a single hash function with 2 parameters (although it would be less efficient I guess). I am thus not interested in solving this specific problem, but rather knowing if "compile time loops" existed for similar cases.

Thanks!

nbonneel
  • 3,286
  • 4
  • 29
  • 39
  • I don't think it's a good answer, so I will just comment here: probably compilers are able to see such things and optimize it - but I'm definitely not an expert on compilers. – Griwes Jul 29 '11 at 12:25
  • before posting, I tried to put a loop, but it didn't work - hence my question ;) – nbonneel Jul 29 '11 at 12:27
  • you could always partially specialize your template, but it's not a good solution either :D – Griwes Jul 29 '11 at 12:28
  • yes, or generate a header file with the command: fprintf(f, "#define STATICFOR(N,C,D)\n switch(N){\n"); for (int i=0; i\n",j,j); } or something similar - this should cover all possibilities until VERYLARGE ;) lol – nbonneel Jul 29 '11 at 12:36
  • your current code doesn't compile; can you give a compilable code ? – iammilind Jul 29 '11 at 12:40
  • indeed, sorry, I removed too much from my whole code. I fixed it now. – nbonneel Jul 29 '11 at 12:46
  • 1
    It's an unusual program which requires the initialisation phase to be optimised at all. – Chris Huang-Leaver Jul 29 '11 at 12:47
  • 1
    @Chris: What makes you think this class is only to be used in the initialisation phase of the program? – pmr Jul 29 '11 at 13:51

6 Answers6

25

Nope, it's not directly possible. Template metaprogramming is a pure functional language. Every value or type defined through it are immutable. A loop inherently requires mutable variables (Repeatedly test some condition until X happens, then exit the loop).

Instead, you would typically rely on recursion. (Instantiate this template with a different template parameter each time, until you reach some terminating condition).

However, that can solve all the same problems as a loop could.

Edit: Here's a quick example, computing the factorial of N using recursion at compile-time:

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);
}

Template metaprogramming in C++ is a Turing-complete language, so as long as you don't run into various internal compiler limits, you can solve basically any problem with it.

However, for practical purposes, it may be worth investigating libraries like Boost.MPL, which contains a large number of data structures and algorithms which simplify a lot of metaprogramming tasks.

jalf
  • 243,077
  • 51
  • 345
  • 550
  • thanks! Do you have an example of how such a recursive template could be implemented? – nbonneel Jul 29 '11 at 12:44
  • just added a simple (untested) example. Note that using it for practical purposes can be somewhat more complicated. :) – jalf Jul 29 '11 at 12:46
  • 1
    With no disrespect for your answer; I think this is **possible**. You can see my answer. – iammilind Jul 29 '11 at 13:10
  • 1
    I'm not sure why that would disrespect my answer. We're both saying his problem can be solved at compile-time, aren't we? ;) – jalf Jul 29 '11 at 13:11
25

Yes. Possible using compile time recursion.

I was trying with your code but since it was not compilable here is a modified and compiling exmaple:

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. The only constraint of this solution is that you have to provide the final specialized version as, CountSketch<int,10>::Init<0> for whatever type and size.

iammilind
  • 68,093
  • 33
  • 169
  • 336
5

You need a combination of boost::mpl::for_each and boost::mpl::range_c.

Note: This will result in run-time code and this is what you actually need. Because there is no way to know the result of operator& at compile time. At least none that I'm aware of.

The actual difficulty with this is to build a struct that is templated on an int parameter (mpl::int_ in our case) and that does the assignment when operator() is called and we also need a functor to actually capture the this pointer.

This is somewhat more complicated than I anticipated but it's fun.

#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;
}
pmr
  • 58,701
  • 10
  • 113
  • 156
  • It's hard to tell, but it looks like that gives you a *run time* loop, not a *compile time* loop. – Gabe Jul 29 '11 at 12:25
  • @Gabe: This is definitely compile-time. After all, "mpl" stands for "meta programming library". – sbi Jul 29 '11 at 13:03
  • @sbi: Nope. It is a run-time loop. `for_each` would be more or less worthless (or at least worth a whole lot less) if it resulted in a compile-time loop. RTFM. It is very clear that both Boost's for_each and the c++0x for_each are run-time constructs. – David Hammen Jul 29 '11 at 13:11
  • 3
    @David: mpl::for_each and std::for_each are fundamentaly different. One is working on a sequence of types the other on a sequence of values. A sequence of types is in every case a compile-time construct. What is meant by mpl::for_each being run-time is that it actually will result in code that is executed at runtime. The actual iteration (actually recursion) will be a compile-time construct. – pmr Jul 29 '11 at 13:21
  • I added the actual implementation. Maybe someone else can make this a little more concise as it is pretty verbose right now and the names for things suck. – pmr Jul 29 '11 at 15:55
4

with C++20 and consteval compile time loops became possible without doing template hell unless the value can have multiple types:

consteval int func() {
    int out = 0;
    for(int i = 10; i--;) out += i;
    return out;
}
int main() {
    std::cout << func(); // outputs 45
}
simamt
  • 125
  • 1
  • 5
  • Tooons of restrictions on this though: https://en.cppreference.com/w/cpp/language/constexpr "A constexpr function must satisfy the following requirements: ..." – Andrew Jul 03 '21 at 04:19
  • This is an answer to a different question. How would you pass the loop index as a template argument? That's the question here. – Arda Mar 24 '22 at 05:59
2

Here is, I think, a better version of the solution given above.
You can see that we use the compile-time recursive on the function params.
This enables putting all the logic inside your class, and the base case of Init(int_<0>) is very clear - just do nothing :)
Just so you won't fear performance penalty, know that the optimizer will throw away these unused parameters.
As a matter of fact, all these function calls will be inlined anyway. that's the whole point here.

#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

ShaulF
  • 841
  • 10
  • 17
2

There are compilers that will see the loop and unroll it. But it's not part of the language specification that it must be done (and, in fact, the language specification throws all sorts of barriers in the way of doing it), and there's no guarantee that it will be done, in a particular case, even on a compiler that "knows how".

There are a few languages that explicitly do this, but they are highly specialized.

(BTW, there's no guarantee that the "unrolled" version of your initializations would be done "at compile time" in a reasonably efficient fashion. But most compilers will, when not compiling to a debug target.)

Hot Licks
  • 47,103
  • 17
  • 93
  • 151
  • Loop unrolling is considered a common optimization in C++ compilers. – pmr Jul 29 '11 at 12:35
  • indeed - but I just have a compile time error (with VS 2008) if I do a loop. – nbonneel Jul 29 '11 at 12:38
  • Yes, "common", but far from guaranteed. And often "unrolling" consists only of laying 2-4 iterations end-to-end, to simply save some addressing calcs. – Hot Licks Jul 29 '11 at 12:39
  • @WhitAngl -- I guess jalf has the "answer" to your problem. I wasn't paying much attention to the template aspect. (Templates make my head hurt.) – Hot Licks Jul 29 '11 at 12:42
  • I'd love to know what those "barriers" are. Can you offer some examples, please? – spraff Jul 29 '11 at 12:57
  • Primarily the order of side-effects. Kind of like the & vs && rules only much less obvious to the untrained eye. Java is one of the worst in this regard, and I've never looked at the C++ spec in detail with regard to the issue, but I'm sure there are a number of problems if you read it with that in mind. – Hot Licks Jul 29 '11 at 15:42