【问题标题】:Template Metaprogramming - I still don't get it :(模板元编程 - 我还是不明白 :(
【发布时间】:2010-11-16 11:52:48
【问题描述】:

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

问题是,我已经阅读了很多关于它的内容,但对我来说仍然没有多大意义。

事实 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
}

所以这个元程序更快......因为常量字面量。

但是现实世界中哪里有常量字面量?我使用的大多数程序都会对用户输入做出反应。

事实编号。 2:模板元编程可以实现更好的可维护性。

是的,阶乘示例可能是可维护的,但是当涉及到复杂的函数时,我和大多数其他 C++ 程序员都无法阅读它们。

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

模板元编程什么时候有意义?

【问题讨论】:

  • 这对我来说从来没有多大意义......
  • 别难过,几乎没人懂。

标签: c++ templates metaprogramming


【解决方案1】:

正如阶乘不是非函数式语言中递归的现实示例,它也不是模板元编程的现实示例。这只是人们想要向您展示递归的标准示例。

在为现实目的编写模板时,例如在日常库中,模板通常必须根据实例化的类型参数来调整它的功能。这可能会变得相当复杂,因为模板有效地有条件地选择要生成的代码。这就是模板元编程。如果模板必须循环(通过递归)并在备选方案之间进行选择,它实际上就像一个在编译期间执行以生成正确代码的小程序。

这是来自 boost 文档页面的一个非常好的教程(实际上是从 brilliant book 中摘录的,非常值得一读)。

http://www.boost.org/doc/libs/1_39_0/libs/mpl/doc/tutorial/representing-dimensions.html

【讨论】:

  • 好答案。 +1 - 我希望人们已经同意一个更好的标准示例。通常,使用元编程来计算值是毫无意义的。当试图弄清楚 TMP 时,它作为一种学习练习很有启发性,但不是作为一个用例,也不是作为说服人们该功能有多棒的卖点。
  • 当然有用,但它并不是说服人们该功能值得的真正大卖点。二进制示例比平时要好,但显而易见的问题是“为什么不只是在运行时执行它?它不像它那样昂贵得令人望而却步。”这真的是在 C++ 之上分层另一种图灵完备语言的理由吗? ;)
  • 按照 Stroustrup 所说的方式,从来没有人决定这样做,所以不需要理由! :) 他们后来才意识到。在您将它们组合成一个复杂的组合之前,这些部件本身就很有用。但我总体上同意你的观点——当你用它构建一个内部 DSL 时,它真的变成了一个有趣的特性。
  • 像往常一样,来自反对者的评论可能很有趣(但话又说回来,它可能不会)。
【解决方案2】:

我为 SSE swizzling 运算符使用模板元编程来优化编译期间的随机播放。

SSE swizzles ('shuffles') 只能被掩码为字节文字(立即值),因此我们创建了一个 'mask merge' 模板类,该模板类在编译时合并掩码,以便发生多个 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,
    };
};

这可以工作并生成出色的代码,而无需生成代码开销和额外的编译时间。

【讨论】:

    【解决方案3】:

    所以这个元程序更快......因为常量字面量。 但是:在现实世界中,我们在哪里有常量字面量? 我使用的大多数程序都会对用户输入做出反应。

    这就是为什么它几乎从未用于值的原因。通常,它用于类型。使用类型来计算和生成新类型。

    有许多实际用途,其中一些您已经熟悉,即使您没有意识到。

    我最喜欢的例子之一是迭代器。它们大多是用通用编程设计的,是的,但是模板元编程在一个地方特别有用:

    修补指针以便它们可以用作迭代器。迭代器必须公开一些 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&lt;T&gt;::type即可。

    而且我认为上述内容在可维护性方面有一些非常明显的好处。您在迭代器上运行的算法只需实现一次。如果没有这个技巧,您将不得不为指针创建一个实现,并为“适当的”基于类的迭代器创建另一个实现。

    boost::enable_if 这样的技巧也很有价值。您有一个函数的重载,应该只为一组特定的类型启用该函数。您可以使用元编程来指定条件并将其传递给enable_if,而不是为每种类型定义重载。

    Earwicker 已经提到了另一个很好的例子,一个表示物理单位和尺寸的框架。它允许您表达计算,例如附加物理单位,并强制执行结果类型。将米乘以米得出平方米数。模板元编程可用于自动生成正确的类型。

    但大多数时候,模板元编程用于(并且有用)小的、孤立的情况,基本上是为了消除颠簸和异常情况,使一组类型的外观和行为一致,允许您更多地使用泛型编程高效

    【讨论】:

      【解决方案4】:

      赞同 Alexandrescu 的 Modern C++ Design 的建议。

      当您编写一个库时,模板真的会大放异彩,其中的片段可以以“选择 Foo、Bar 和 Baz”的方式组合起来,并且您希望用户以某种形式使用这些片段在编译时固定。例如,我与人合着了一个数据挖掘库,该库使用模板元编程让程序员决定使用什么DecisionType(分类、排名或回归),期望InputType 做什么(浮点数、整数、枚举值等等),以及使用什么KernelMethod(这是一个数据挖掘的东西)。然后我们为每个类别实现了几个不同的类,这样就有几十种可能的组合。

      实现 60 个单独的类来执行此操作会涉及大量烦人且难以维护的代码重复。模板元编程意味着我们可以将每个概念实现为一个代码单元,并为程序员提供一个简单的接口,用于在编译时实例化这些概念的组合。

      维度分析也是一个很好的例子,但其他人已经涵盖了这一点。

      我也曾经写过一些简单的编译时伪随机数生成器,只是为了弄乱人们的头脑,但这并不算 IMO。

      【讨论】:

      • 玩弄人的脑袋是编程的最佳用途之一!不要低估它的价值。 :p
      【解决方案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。

      【讨论】:

        【解决方案6】:

        这是一个简单的例子,一个二进制常量转换器,来自 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】:

          TMP 并不一定意味着更快或更易于维护的代码。我使用 boost spirit 库来实现一个简单的 SQL 表达式解析器,它构建了一个评估树结构。虽然由于我对 TMP 和 lambda 有一定的了解,所以开发时间减少了,但学习曲线对于“C with classes”开发人员来说是一堵砖墙,而且性能不如传统的 LEX/YACC。

          我认为模板元编程只是我工具带中的另一个工具。当它适合你时使用它,如果它不起作用,请使用其他工具。

          【讨论】:

            【解决方案8】:

            Scott Meyers 一直致力于使用 TMP 实施代码约束。

            读起来很不错:
            http://www.artima.com/cppsource/codefeatures.html

            在这篇文章中,他介绍了 Sets of Types 的概念(不是一个新概念,但他的作品是基于这个概念的)。然后使用 TMP 确保无论您指定集合成员的顺序如何,如果两个集合由相同的成员组成,那么它们是等价的。这要求他能够对类型列表进行排序和重新排序并动态比较它们,从而在它们不匹配时生成编译时错误。

            【讨论】:

            • 其实他的名字是“迈耶斯”。 (很抱歉吹毛求疵。)
            • 已更正。 :-)
            【解决方案9】:

            我建议你阅读 Andrei Alexandrescu 的Modern C++ Design——这可能是关于 C++ 模板元编程实际使用的最佳书籍之一;并描述了许多问题,C++ 模板是一个很好的解决方案。

            【讨论】:

            • 每个 C++ 开发人员都应该阅读这本书,但这对回答这个问题并没有太大帮助,原因与简单地提供 Google 链接通常不受欢迎的原因相同。 SO 应该是一个寻找答案的地方,而不是指向您可以尝试搜索的其他资源的链接。
            • Scott Meyers 的 Effective C++ 第 3 版还包含一个解释得相当透彻的示例。 (虽然这是关于函数重载而不是元函数,因此起初可能看起来不像“真正的 TMP”。)
            【解决方案10】:

            TMP 可用于确保尺寸正确性(确保质量不能除以时间,但距离可以除以时间并分配给速度变量)到通过删除临时对象和合并循环来优化矩阵运算。涉及矩阵。

            【讨论】:

            • 质量可以除以时间。结果必须是“每次质量”类型;)但是,可以使用 TMP 来强制执行。
            • 任何东西都可以被任何东西除/乘,加法/减法会让人绊倒。
            【解决方案11】:

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

            【讨论】:

              猜你喜欢
              • 2011-06-26
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2014-08-19
              • 2013-04-12
              • 1970-01-01
              • 2011-12-19
              相关资源
              最近更新 更多