了解 C++ 模板元编程
Understanding C++ template 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
),分别是两个(A<T...>, B
)类型参数?
我知道它将 type
别名 (using type = B<T...>;
) 为 B<T...>
而不是 A<T...>
,但我真的不明白它是如何完成的。
另外为什么 A
模板模板参数只在专业化中?
[*] 肯定是我这里有问题
Why is mp_rename_impl
forward declared with two type parameters (class A, template<class...> class B
), then it's defined and specialized at the same time[*] with three (template<class...> class A, class... T, template<class...> class B
) and respectively two(A<T...>, B
) type parameters?
前向声明确定实例化所需的参数数量 mp_rename_impl
,并且前者应该是实际类型,后者是模板。
然后当有一个实际的实例化时,它会尝试匹配特化 struct mp_rename_impl<A<T...>, B>
,并且在这样做时它可以考虑特化的 A
、T...
和 T...
的值的任意组合B
符合专业的期望:即 template<class...> class A, class... T, template<class...> class B
。请注意,特化中的 A
参数与声明中的 A
共享一个名称,但并不相同——前者是模板,后者是类型。实际上,为了匹配特化,模板实例化必须作为声明的 A
参数传递,并且该模板的参数在 T...
处被捕获。它对可以作为 B
传递的内容没有施加新的限制(尽管 using
语句确实 - B<T...>
需要有效,否则你会得到一个编译错误 - SFINAE 来不及了开始)。
Also why is A
a template template parameter only in the specialization?
专业化调用该参数 A
,但它在概念上与声明中的 A
不同。相反,前者的 A<T...>
对应后者 A
。也许专业化应该称它为 "TA" 或其他名称,以表明它是一个模板,可以从中结合 T...
参数形成实际的 A
。
然后是 A<T...>, B
的特化,因此编译器从实际尝试找到 A
、T...
和 B
的任何实例化开始逆向工作,以限制为指导它们的形式在 template<template<class...> class A, class... T, template<class...> class B>
.
中指定
这是为了确保只有当两个参数是一个已经给定了一些参数类型集的模板和一个能够采用参数类型列表的模板时才匹配专业化。匹配过程有效地隔离了 T
类型列表,因此它可以与 B
.
重用
我的第一次尝试不是你想要的,所以让我简短地尝试回去解释一下,就像你六岁一样。
在函数具有原型和定义的意义上,它不是 forward-declared。任何 A 都有一个实现,它编译为一个空结构(这是编译器的唯一类型,但不需要任何实际存储或 run-time 代码)。然后,还有第二种实现,只针对模板 classes A.
第二个定义中确实有两个模板。发生的事情是第二个定义它接受两个参数 A
和 ... T
并将它们变成类型 A<T>
,这成为 mp_rename_impl<A<T...>,B>
的第一个参数。所以它适用于任何 A
模板 class。但这是一种更具体的 A
!所以它是一个特殊化,需要在其范围内声明一个具有类型定义的结构。最后,第三个变体根本不是模板的特化。它在第二个声明中将模板化 mp_rename
声明为存储在每个结构范围内的更复杂类型的别名,如您所见,它是范围 mp_rename_impl<A, B>
中的标识符 type
。信不信由你,这让他的模板代码更具可读性。
为什么顶部更通用的定义扩展为一个空结构?当 A
不是模板 class 时,内容很简单,但它的名称确实需要某种类型,因此编译器会认为它与所有其他类型不同。 (在下面编写我的示例以生成使用静态常量作为成员而不是函数的 classes 会更酷。事实上,我只是这样做了。)
已更新,威胁要让我的模板更像他的模板:
好的,模板元编程是一种编程,在这种编程中,编译器不会在 运行s 时让程序计算某些东西,而是提前计算它并将答案存储在程序中。它通过编译模板来做到这一点。有时,这比 运行 快得多!但是你能做的是有限的。主要是,你不能修改任何参数,你必须确保计算停止。
如果你在想,“你是说,就像函数式编程一样?”你是一个非常聪明的人 five-year-old。您通常最终要做的是编写带有基本案例的递归模板,这些基本案例可以扩展为展开的、简化的代码或常量。这是您三岁或四岁时计算机科学入门 class 中的一个示例:
#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 586269025, %rsi
模板在 compile-time 处的每个结构中生成一个整数常量。这些示例正在做的事情,因为您可以在结构中声明类型名称,对类型做同样的事情。
我会尽量简化。模板元编程是关于 compile-time 处的计算类型(您也可以计算值,但让我们专注于此)。
所以如果你有这个功能:
int f(int a, int b);
你有一个函数,return给定两个 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
。
因此您在 non-generic 上下文中调用这样的元函数:
using my_new_type = meta_f<int, float>::type;
在通用上下文中,您必须使用 typename
:
using my_new_type = typename meta_f<T, U>::type;
这 return 是一个类型,而不是 run-time 值,因为我们说过元函数对类型进行操作。
标准库中元函数的例子可以在header中找到
type_traits
,等等。您有 add_pointer<T>
或 decay<T>
。这些元函数 return 给定类型的新类型。
在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>;
因此,对于第一个函数,您获得一个值,对于其他函数,您获得一个类型,而不是一个值。
为了更好地理解 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
),分别是两个(A<T...>, B
)类型参数?
我知道它将 type
别名 (using type = B<T...>;
) 为 B<T...>
而不是 A<T...>
,但我真的不明白它是如何完成的。
另外为什么 A
模板模板参数只在专业化中?
[*] 肯定是我这里有问题
Why is
mp_rename_impl
forward declared with two type parameters (class A, template<class...> class B
), then it's defined and specialized at the same time[*] with three (template<class...> class A, class... T, template<class...> class B
) and respectively two(A<T...>, B
) type parameters?
前向声明确定实例化所需的参数数量 mp_rename_impl
,并且前者应该是实际类型,后者是模板。
然后当有一个实际的实例化时,它会尝试匹配特化 struct mp_rename_impl<A<T...>, B>
,并且在这样做时它可以考虑特化的 A
、T...
和 T...
的值的任意组合B
符合专业的期望:即 template<class...> class A, class... T, template<class...> class B
。请注意,特化中的 A
参数与声明中的 A
共享一个名称,但并不相同——前者是模板,后者是类型。实际上,为了匹配特化,模板实例化必须作为声明的 A
参数传递,并且该模板的参数在 T...
处被捕获。它对可以作为 B
传递的内容没有施加新的限制(尽管 using
语句确实 - B<T...>
需要有效,否则你会得到一个编译错误 - SFINAE 来不及了开始)。
Also why is
A
a template template parameter only in the specialization?
专业化调用该参数 A
,但它在概念上与声明中的 A
不同。相反,前者的 A<T...>
对应后者 A
。也许专业化应该称它为 "TA" 或其他名称,以表明它是一个模板,可以从中结合 T...
参数形成实际的 A
。
然后是 A<T...>, B
的特化,因此编译器从实际尝试找到 A
、T...
和 B
的任何实例化开始逆向工作,以限制为指导它们的形式在 template<template<class...> class A, class... T, template<class...> class B>
.
这是为了确保只有当两个参数是一个已经给定了一些参数类型集的模板和一个能够采用参数类型列表的模板时才匹配专业化。匹配过程有效地隔离了 T
类型列表,因此它可以与 B
.
我的第一次尝试不是你想要的,所以让我简短地尝试回去解释一下,就像你六岁一样。
在函数具有原型和定义的意义上,它不是 forward-declared。任何 A 都有一个实现,它编译为一个空结构(这是编译器的唯一类型,但不需要任何实际存储或 run-time 代码)。然后,还有第二种实现,只针对模板 classes A.
第二个定义中确实有两个模板。发生的事情是第二个定义它接受两个参数 A
和 ... T
并将它们变成类型 A<T>
,这成为 mp_rename_impl<A<T...>,B>
的第一个参数。所以它适用于任何 A
模板 class。但这是一种更具体的 A
!所以它是一个特殊化,需要在其范围内声明一个具有类型定义的结构。最后,第三个变体根本不是模板的特化。它在第二个声明中将模板化 mp_rename
声明为存储在每个结构范围内的更复杂类型的别名,如您所见,它是范围 mp_rename_impl<A, B>
中的标识符 type
。信不信由你,这让他的模板代码更具可读性。
为什么顶部更通用的定义扩展为一个空结构?当 A
不是模板 class 时,内容很简单,但它的名称确实需要某种类型,因此编译器会认为它与所有其他类型不同。 (在下面编写我的示例以生成使用静态常量作为成员而不是函数的 classes 会更酷。事实上,我只是这样做了。)
已更新,威胁要让我的模板更像他的模板:
好的,模板元编程是一种编程,在这种编程中,编译器不会在 运行s 时让程序计算某些东西,而是提前计算它并将答案存储在程序中。它通过编译模板来做到这一点。有时,这比 运行 快得多!但是你能做的是有限的。主要是,你不能修改任何参数,你必须确保计算停止。
如果你在想,“你是说,就像函数式编程一样?”你是一个非常聪明的人 five-year-old。您通常最终要做的是编写带有基本案例的递归模板,这些基本案例可以扩展为展开的、简化的代码或常量。这是您三岁或四岁时计算机科学入门 class 中的一个示例:
#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 586269025, %rsi
模板在 compile-time 处的每个结构中生成一个整数常量。这些示例正在做的事情,因为您可以在结构中声明类型名称,对类型做同样的事情。
我会尽量简化。模板元编程是关于 compile-time 处的计算类型(您也可以计算值,但让我们专注于此)。
所以如果你有这个功能:
int f(int a, int b);
你有一个函数,return给定两个 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
。
因此您在 non-generic 上下文中调用这样的元函数:
using my_new_type = meta_f<int, float>::type;
在通用上下文中,您必须使用 typename
:
using my_new_type = typename meta_f<T, U>::type;
这 return 是一个类型,而不是 run-time 值,因为我们说过元函数对类型进行操作。
标准库中元函数的例子可以在header中找到
type_traits
,等等。您有 add_pointer<T>
或 decay<T>
。这些元函数 return 给定类型的新类型。
在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>;
因此,对于第一个函数,您获得一个值,对于其他函数,您获得一个类型,而不是一个值。