繁体   English   中英

了解C ++模板元编程

[英]Understanding C++ template metaprogramming

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

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 ++有一般性和基本的理解。

我没有得到的是:

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

我理解它别名( using type = B<T...>;typeB<T...>而不是A<T...> ,但我真的不明白它是如何完成的。

另外,为什么A模板模板参数仅在专业化中?

[*]我肯定在这里弄错了

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

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

然后当有一个实际的实例化时,它会尝试匹配特化struct mp_rename_impl<A<T...>, B> ,并且这样做可以考虑专业化的AT...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 也许专业化应该称之为“TA”或其他东西来表明它是一个模板,从中可以结合T...参数形成实际的A 然后专门化为A<T...>, B ,因此编译器从实际尝试的任何实例化向后工作,以找到AT...B有效替换,由template<template<class...> class A, class... T, template<class...> class B>指定的形式的限制引导template<template<class...> class A, class... T, template<class...> class B>

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

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

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

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

为什么顶部更通用的定义扩展为空结构? 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

模板在编译时在每个结构内生成一个整数常量。 这些示例正在做什么,因为您可以在结构中声明类型名称,对类型执行相同的操作。

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

所以如果你有这个功能:

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>;

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM