我想知道是否有可能有一些编译时循环 例如,我有以下模板类:
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个参数创建一个哈希函数(虽然我猜它的效率会低一些)。因此我对解决这个特定问题不感兴趣,而是知道类似情况下是否存在“编译时循环”。
谢谢!
答案 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_each和boost::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
}