模板元编程 - 我仍然没有得到它:(

时间:2009-08-03 19:56:46

标签: c++ templates metaprogramming

我有问题...我不懂模板元编程。

问题是这样的:我读了很多。但这对我来说没有多大意义:/

事实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 ++程序员都无法读取函数。

调试选项很差(或者至少我不知道如何调试)。

模板元编程在哪里有意义?

11 个答案:

答案 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是关于在编译时执行算法的,这意味着一些明显的优势:

  • 由于这些算法失败意味着您的代码无法编译,失败的算法永远不会让您的客户,因此不会失败的客户。对我来说,在过去十年中,这是最重要的一个优势,这使我将TMP引入我所工作的公司的代码中。
  • 由于执行模板元程序的结果是由编译器编译的普通代码,因此代码生成算法的所有优点(减少冗余等)都适用。
  • 当然,由于它们是在编译时执行的,因此这些算法不需要任何运行时间,因此运行速度更快。 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上的上一个问题:

C++ binary constant/literal

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并实例化不同可能性的代码。