我有问题...我不懂模板元编程。
问题是这样的:我读了很多。但这对我来说没有多大意义:/
事实nr.1 :模板元编程更快
template <int N>
struct Factorial
{
enum { value = N * Factorial<N - 1>::value };
};
template <>
struct Factorial<0>
{
enum { value = 1 };
};
// Factorial<4>::value == 24
// Factorial<0>::value == 1
void foo()
{
int x = Factorial<4>::value; // == 24
int y = Factorial<0>::value; // == 1
}
所以这个Metaprogram更快......因为Constant Literal。
但是:真实世界中的哪些地方我们有不变的文字?
我使用的大多数程序都会对用户输入作出反应。
FACT nr。 2 :模板元编程可以实现更好的可维护性。
呀。阶乘示例可以维护......但是当涉及到复杂的函数时,我和大多数其他C ++程序员都无法读取函数。
调试选项很差(或者至少我不知道如何调试)。
模板元编程在哪里有意义?
答案 0 :(得分:27)
正如因子不是非函数语言递归的现实例子,它也不是模板元编程的现实例子。这只是人们想要向您展示递归的标准示例。
在为实际目的编写模板时,例如在日常库中,模板通常必须根据实例化的类型参数调整它的功能。这可能变得非常复杂,因为模板有条件地有效地选择要生成的代码。这就是模板元编程的原因;如果模板必须循环(通过递归)并在备选之间进行选择,它实际上就像在编译期间执行以生成正确代码的小程序。
这是来自boost文档页面的一个非常好的教程(实际上摘自brilliant book,非常值得一读)。
http://www.boost.org/doc/libs/1_39_0/libs/mpl/doc/tutorial/representing-dimensions.html
答案 1 :(得分:15)
我使用模板mete-programming为SSE调配操作符在编译期间优化shuffle。
SSE swizzles('shuffles')只能被屏蔽为字节文字(立即值),所以我们创建了一个'mask merger'模板类,它在编译时合并掩码,以便在多次shuffle发生时:
template <unsigned target, unsigned mask>
struct _mask_merger
{
enum
{
ROW0 = ((target >> (((mask >> 0) & 3) << 1)) & 3) << 0,
ROW1 = ((target >> (((mask >> 2) & 3) << 1)) & 3) << 2,
ROW2 = ((target >> (((mask >> 4) & 3) << 1)) & 3) << 4,
ROW3 = ((target >> (((mask >> 6) & 3) << 1)) & 3) << 6,
MASK = ROW0 | ROW1 | ROW2 | ROW3,
};
};
这样可以生成非凡的代码,而不会产生代码开销和额外的编译时间。
答案 2 :(得分:9)
所以这个Metaprogram更快......因为Constant Literal。 但是:在现实世界中我们有不变的文字吗? 我使用的大多数程序都会对用户输入作出反应。
这就是为什么它几乎没有用于价值观。通常,它用于类型。使用类型来计算和生成新类型。
有许多实际用途,即使你没有意识到,其中一些你已经熟悉了。
我最喜欢的一个例子是迭代器。他们大多只是使用通用编程设计,是的,但模板元编程特别适用于某个地方:
修补指针,使它们可以用作迭代器。迭代器必须暴露一些typedef,例如value_type
。指针不这样做。
所以代码如下(基本上与你在Boost.Iterator中找到的相同)
template <typename T>
struct value_type {
typedef typename T::value_type type;
};
template <typename T>
struct value_type<T*> {
typedef T type;
};
是一个非常简单的模板元程序,但它非常有用。它允许您获取任何迭代器类型T的值类型,无论它是指针还是类,只需value_type<T>::type
。
我认为上述在可维护性方面有一些非常明显的好处。只在迭代器上运行的算法必须实现一次。如果没有这个技巧,你必须为指针做一个实现,而另一个用于“适当的”基于类的迭代器。
像boost::enable_if
这样的技巧也非常有价值。您有一个函数的重载,应该只为特定的类型集启用。您可以使用元编程来指定条件并将其传递给enable_if
,而不是为每种类型定义重载。
Earwicker已经提到了另一个很好的例子,一个表达物理单位和维度的框架。它允许您表达附加物理单位的计算,并强制执行结果类型。乘以米为单位的平均米数。模板元编程可用于自动生成正确的类型。
但是大多数时候,模板元编程在小的,孤立的情况下使用(并且很有用),基本上是为了平滑凸起和异常情况,使一组类型看起来和行为一致,允许你更多地使用泛型编程有效地
答案 3 :(得分:9)
支持Alexandrescu的现代C ++设计的建议。
当你正在编写一个可以在“选择Foo,Bar和Baz”方法中组合组装的库时,模板真的很闪耀,并且您希望用户以某种形式使用这些片段在编译时固定。例如,我合着了一个数据挖掘库,它使用模板元编程让程序员决定使用DecisionType
(分类,排名或回归),期望InputType
(浮点数,整数,枚举值,无论如何)和KernelMethod
使用什么(它是一个数据挖掘的东西)。然后,我们为每个类别实现了几个不同的类,以便有几十种可能的组合。
实现60个单独的类来执行此操作将涉及许多烦人的,难以维护的代码重复。模板元编程意味着我们可以将每个概念实现为代码单元,并为程序员提供一个简单的接口,用于在编译时实例化这些概念的组合。
维度分析也是一个很好的例子,但其他人已经涵盖了这一点。
我曾经写过一些简单的编译时伪随机数生成器,只是为了弄乱人们的脑袋,但这并不能真正算上IMO。
答案 4 :(得分:5)
因子示例对于真实世界的TMP和“Hello,world!”一样有用。用于通用编程:它是为了向您展示一些有用的技术(递归而不是迭代,“else-if-then”等),这是一个非常简单,相对容易理解的示例,与您的每一个都没有多大关系 - 编码。 (你最后一次编写一个发出“Hello,world”的程序是什么时候?)
TMP是关于在编译时执行算法的,这意味着一些明显的优势:
当然,也有缺点:
与往常一样,在每种情况下,您都必须权衡利弊。
对于更有用的示例:一旦掌握了类型列表和基于它们的基本编译时算法,您可能会理解以下内容:
typedef
type_list_generator< signed char
, signed short
, signed int
, signed long
>::result_type
signed_int_type_list;
typedef
type_list_find_if< signed_int_type_list
, exact_size_predicate<8>
>::result_type
int8_t;
typedef
type_list_find_if< signed_int_type_list
, exact_size_predicate<16>
>::result_type
int16_t;
typedef
type_list_find_if< signed_int_type_list
, exact_size_predicate<32>
>::result_type
int32_t;
这是(几乎简化)我几周前写的实际代码。它将从类型列表中选择适当的类型,替换便携式代码中常见的#ifdef
orgies。它不需要维护,无需适应您的代码可能需要移植到的每个平台,如果当前平台没有正确的类型,则会发出编译错误。
另一个例子是:
template< typename TFunc, typename TFwdIter >
typename func_traits<TFunc>::result_t callFunc(TFunc f, TFwdIter begin, TFwdIter end);
给定一个函数f
和一系列字符串,这将解析函数的签名,将序列中的字符串转换为正确的类型,并使用这些对象调用函数。它主要是TMP内部。
答案 5 :(得分:3)
Scott Meyers一直致力于使用TMP强制执行代码约束。
非常好读:
http://www.artima.com/cppsource/codefeatures.html
在本文中,他介绍了类型集的概念(不是一个新概念,但他的工作是基于这个概念的顶部)。然后使用TMP确保无论您指定集合的成员是什么顺序,如果两个集合由相同的成员组成,那么它们是等价的。这要求他能够对类型列表进行排序和重新排序,并动态比较它们,从而在不匹配时生成编译时错误。
答案 6 :(得分:3)
这是一个简单的例子,一个二进制常量转换器,来自StackOverflow上的上一个问题:
template< unsigned long long N >
struct binary
{
enum { value = (N % 10) + 2 * binary< N / 10 > :: value } ;
};
template<>
struct binary< 0 >
{
enum { value = 0 } ;
};
答案 7 :(得分:3)
TMP并不一定意味着更快或更易维护的代码。我使用boost spirit库来实现一个构建评估树结构的简单SQL表达式解析器。由于我对TMP和lambda有一定的了解,开发时间减少了,学习曲线是“C with classes”开发人员的砖墙,性能不如传统的LEX / YACC。
我认为模板元编程只是我工具带中的另一个工具。当它适用于您时,如果没有,请使用其他工具。
答案 8 :(得分:2)
我建议您阅读Andrei Alexandrescu撰写的Modern C++ Design - 这可能是关于C ++模板元编程实际使用的最佳书籍之一;并描述了C ++模板是一个很好的解决方案的许多问题。
答案 9 :(得分:2)
TMP可用于确保尺寸正确性(确保质量不能按时间划分,但距离可按时间划分并分配给速度变量),以通过删除临时对象和合并循环来优化矩阵操作涉及矩阵。
答案 10 :(得分:2)
'static const'值也可以。和指向成员的指针。并且不要忘记类型的世界(显式和推导)作为编译时参数!
但是:在现实世界中我们有不变的文字吗?
假设您有一些代码必须尽可能快地运行。它实际上包含CPU绑定计算的关键内部循环。您可能愿意稍微增加可执行文件的大小以使其更快。它看起来像:
double innerLoop(const bool b, const vector<double> & v)
{
// some logic involving b
for (vector::const_iterator it = v.begin; it != v.end(); ++it)
{
// significant logic involving b
}
// more logic involving b
return ....
}
细节并不重要,但在实施过程中使用'b'是普遍存在的。
现在,使用模板,您可以稍微重构一下:
template <bool b> double innerLoop_B(vector<double> v) { ... same as before ... }
double innerLoop(const bool b, const vector<double> & v)
{ return b ? innerLoop_templ_B<true>(v) : innerLoop_templ_B<false>(v) ); }
如果您有一个相对较小的离散值参数值,您可以自动为它们实例化单独的版本。
考虑“b”基于CPU检测时的可能性。您可以根据运行时检测运行不同优化的代码集。所有来自相同的源代码,或者您可以为某些值集合专门化一些函数。
作为一个具体的例子,我曾经看过一些需要合并一些整数坐标的代码。坐标系'a'是两种分辨率之一(在编译时已知),坐标系'b'是两种不同分辨率之一(在编译时也已知)。目标坐标系需要是两个源坐标系的最小公倍数。在编译时使用库来计算LCM并实例化不同可能性的代码。