了解C ++模板元编程

时间:2015-09-08 05:23:03

标签: c++ templates c++11 metaprogramming

为了更好地理解C ++中的模板和元编程,我正在阅读this article,但我对代码片段的理解很快就会消失,例如:

template<class A, template<class...> class B> struct mp_rename_impl;

template<template<class...> class A, class... T, template<class...> class B>
    struct mp_rename_impl<A<T...>, B>
{
    using type = B<T...>;
};

template<class A, template<class...> class B>
    using mp_rename = typename mp_rename_impl<A, B>::type;

代码用作:

mp_rename<std::pair<int, float>, std::tuple>        // -> std::tuple<int, float>
mp_rename<mp_list<int, float>, std::pair>           // -> std::pair<int, float>
mp_rename<std::shared_ptr<int>, std::unique_ptr>    // -> std::unique_ptr<int>

有人可以像我这样解释代码吗? 我对非模板化C ++有一般性和基本的理解。

我得不到的是:

为什么mp_rename_impl使用两个类型参数(class A, template<class...> class B)声明前转,然后它同时定义并专门设置[*]三个(template<class...> class A, class... T, template<class...> class B)和两个({{ {1}})输入参数?

我理解它将A<T...>, B的{​​{1}}别名为using type = B<T...>;而不是type,但我真的不明白它是如何完成的。

为什么B<T...>只是专业化中的模板模板参数?

[*]我肯定错在这里

3 个答案:

答案 0 :(得分:4)

  

为什么使用两个类型参数(mp_rename_impl)声明class A, template<class...> class B转发,然后它同时定义并专门设置[*]三个(template<class...> class A, class... T, template<class...> class B)和分别有两个(A<T...>, B)类型参数?

前向声明确定了实例化mp_rename_impl所需的参数数量,前者应该是实际类型,后者是模板。

然后,当有实际的实例化时,它会尝试匹配专业化struct mp_rename_impl<A<T...>, B>,并且这样做可以考虑专业化A的值的任意组合, T...B符合专业化的期望:template<class...> class A, class... T, template<class...> class B。请注意,特化中的A参数在声明中与A共享一个名称,但不一样 - 前者是模板,后者是类型。实际上,为了匹配特化,必须将模板实例化作为声明的A参数传递,并且在T...处捕获该模板的参数。它对B传递的内容没有新的限制(虽然using语句确实 - B<T...>需要有效,否则您将收到编译错误 - 对于SFINAE开始)。

  

为什么A只是专业化中的模板模板参数?

专门化调用参数A,但它在概念上与声明中的A不同。相反,前者A<T...>对应后者A。也许专业化应该称之为&#34; TA&#34;或其他东西,以表明它可以与A参数组合形成实际T...的模板。 然后专门化为A<T...>, B,因此编译器会从实际尝试的任何实例化向后工作,以查找AT...B的有效替换,并受限于他们的表格在template<template<class...> class A, class... T, template<class...> class B>中指定。

这样做可以确保专业化仅在两个参数是已经给出一些参数类型的模板时匹配,并且模板能够获取参数类型列表。匹配过程有效地隔离了T类型列表,因此可以使用B重复使用。

答案 1 :(得分:1)

我的第一次尝试不是你想要的,所以让我简单地试着回过头来解释一下,就像你六岁一样。

在函数具有原型和定义的意义上,它不是前向声明的。有一个实现任何A,并编译为一个空结构(这是编译器的唯一类型,但不需要任何实际的存储或运行时代码)。然后,有第二个实现,仅适用于模板类A。

第二个定义中确实有两个模板。发生了什么,第二个定义是两个参数A... T并将它们转换为A<T>类型,它成为mp_rename_impl<A<T...>,B>的第一个参数。因此它适用于任何A ,它是模板类。但这是一种更具体的A!所以它是一个特殊化,需要在其范围内声明一个带有类型定义的结构。最后,第三个变体根本不是模板的特化。它将模板mp_rename声明为第二个声明中每个结构范围内存储的更复杂类型的别名,如您所见,范围type中的标识符为mp_rename_impl<A, B>。信不信由你,这使他的模板代码更具可读性。

为什么顶部更通用的定义扩展为空结构?当A不是模板类时,内容是微不足道的,但它确实需要某种类型的名称,因此编译器会认为它与其他所有类型都不同。 (将下面的示例编写为使用静态常量作为成员而不是函数生成类会更酷。事实上,我只是这样做。)

更新为威胁我的模板更像他的:

好的,模板元编程是一种编程,其中编译器在运行时不是让程序计算某些东西,而是提前计算它并将答案存储在程序中。它通过编译模板来实现。运行起来要快得多,有时候!但是你可以做什么是有限的。主要是,你不能修改任何参数,你必须确保计算停止。

如果你在想,“你的意思是,就像功能性编程一样?”你是一个非常聪明的五岁孩子。您通常最终要做的是编写具有基本案例的递归模板,这些基本案例可以扩展为展开的,简化的代码或常量。这是一个例子,当你三岁或四岁时,你的计算机科学入门班可能会很熟悉:

#include <iostream>

using std::cout;
using std::endl;

/* The recursive template to compute the ith fibonacci number.
 */
template < class T, unsigned i >
  struct tmp_fibo {
    static const T fibo = tmp_fibo<T,i-1>::fibo + tmp_fibo<T,i-2>::fibo;
  };

/* The base cases for i = 1 and i = 0.  Partial struct specialization
 * is allowed.
 */
template < class T >
  struct tmp_fibo<T,1U> {
    static const T fibo = (T)1;
  };

template < class T >
  struct tmp_fibo<T,0U> {
    static const T fibo = (T)0;
  };

int main(void) {
  cout << "fibo(50) = " << tmp_fibo<unsigned long long, 50>::fibo
       << ". fibo(10) = " << tmp_fibo<int, 10>::fibo << "."
       <<  endl;

  return 0;
}

编译汇编语言,我们看到编译器为行tmp_fibo<unsigned long long, 50>::fibo生成了什么代码。完整地说:

movabsq $12586269025, %rsi

模板在编译时在每个结构中生成一个整数常量。这些例子正在做什么,因为你可以在结构中声明类型名称,对类型做同样的事情。

答案 2 :(得分:1)

我会尽量简单。模板元编程是关于在编译时计算类型的(你也可以计算值,但我们只关注它)。

所以如果你有这个功能:

int f(int a, int b);

你有一个函数,它返回给定两个int值的int值。

你这样使用它:

int val = f(5, 8);

元功能对类型进行操作,而不是对值进行操作。元函数看起来像这样:

//The template parameters of the metafunction are the
//equivalent of the parameters of the function
template <class T, class U>
struct meta_f {
    typedef /*something here*/ type;
};

即,元函数内部嵌套type按惯例嵌套类型称为type

所以你在非通用上下文中调用这样的元函数:

using my_new_type = meta_f<int, float>::type;

在通用上下文中,您必须使用typename

using my_new_type = typename meta_f<T, U>::type;

这会返回一个类型,而不是运行时值,因为我们说元函数对类型进行操作。

标题库中的元函数示例可以在标题中找到 type_traits等等。您有add_pointer<T>decay<T>。这些元函数返回给定类型的新类型。

在C ++ 14中,为了避免这样的代码片段,这些代码很详细:

using my_computed_type = typename std::add_pointer<T>::type;

创建了一些带有_t后缀的模板别名,按照惯例,直接为您调用元函数:

template <class T>
using add_pointer_t = typename std::add_pointer<T>::type;

现在你可以写:

using my_computed_type = std::add_pointer_t<T>;

总而言之,在函数中,您将运行时值作为参数,在元函数中,参数是类型。在您调用的函数中 通常的语法并获取运行时值。在元函数中,您获得::type嵌套类型并获得新的计算类型。

//Function invocation, a, b, c are values of type A, B, C
auto ret = f(a, b, c);

//Meta function invocation. A, B, C are types
using ret_t = typename meta_f<A, B, C>::type;

//Typical shortcut, equivalent to metafunction invocation.
using ret_t = meta_f_t<A,B,C>;

因此,对于第一个函数,您获得一个值,对于其他函数,您将获得一个类型,而不是一个值。