引言
引用任何足够复杂的C++代码都与钓鱼无法区分
阿瑟·克拉克
前言
模板元编程是让C++如此复杂、鲜为人知,有时甚至是糟糕的语言的原因之一。然而,它的强大和表现力是C++最好的特性之一。
没有模板元编程,就无法实现可扩展和完全通用的C++库。甚至标准库的实现也隐藏了许多模板元编程技巧,以便使标准容器和算法成为我们日常使用的如此通用、高级和高效的工具。
tmp是一种强大工具的事实,可以通过语言的演变来看,现在已经有了旨在改进元编程的特性,例如C++11的<type_traits>
、C++11的变长模板、C++14的变量模板、C++14的std::integer_sequence
等。
但是C++模板元编程的力量是以高昂的代价换来的:它非常难于编写和理解。模板系统最初并非为此类目的而设计,这主要体现在繁琐的语法和出现错误时令人费解的错误消息。这些是人们通常害怕tmp的原因,我们大多数人甚至不尝试使用它。
本文旨在向普通的C++程序员介绍模板元编程,展示它是如何工作的,它可以做什么,并最终探讨它在C++11和C++14语言改进的帮助下,如何比早期C++98时代更易于使用。
但是,**元编程**是什么?
来自维基百科
引用
元编程是编写计算机程序,这些程序将其他程序(或自身)作为其数据来编写或操作,或者在编译时完成一些本应在运行时完成的工作。
因此,我们编写的代码(**元代码?**)而不是编译后在运行时执行某些操作的代码(即表示要在运行时执行的某些操作),而是编写代码来生成代码。让我给你们看一个简单的例子。
#define MIN(x,y) (((x) > (y)) ? (x) : (y))
C的参数化宏可以看作是元编程函数,**元函数**。也就是说,一个接受一些参数并生成C代码的函数。如果你使用那个宏
int main() { int a , b , c = MIN(a,b); }
请忽略未定义行为,这只是一个示例。 C预处理器会解析该宏,解释其参数,并返回代码(((a) > (b)) ? (a) : (b))
,因此生成的代码将变成
int main() { int a , b , c = (((a) < (b)) ? (a) : (b)); }
反射,一些编程语言在运行时检查和修改类型和代码信息的能力,可以是另一种类型的元编程。
C++模板元编程
模板元编程,有时简称为**tmp**,是通过**使用C++模板系统来生成C++类型,并在过程中生成C++代码**。考虑一下C++模板是什么:顾名思义,**它只是一个模板**。模板函数根本不是一个函数,它是**一个生成函数的模板**,类模板也是如此。我们都喜欢的那个奇妙的std::vector
,不是一个类。它是一个模板,用于为每种类型生成一个正确的vector类。当我们**实例化**一个模板时,例如std::vector<int>
,编译器就会根据标准库开发者提供的模板,为int的vector生成代码。所以,如果我们写一个以类型参数化的模板foo
template<typename T> struct foo { T elem; };然后那个模板被实例化
typedef foo<int> fooint; typedef foo<char> foochar;编译器就会为**每种不同的模板参数组合**生成不同的
foo
结构版本struct foo_int { int elem; }; struct foo_char { char elem; }; typedef foo_int fooint; typedef foo_char foochar;
请注意,生成的类foo_int
和foo_char
根本不在你的源文件中,就像C预处理器所做的那样。模板实例化是由编译器内部管理的。我这样写是为了举一个清晰的例子。 正如你所见,C++模板系统实际上是在生成代码。我们作为C++**元程序员**,利用这一点来自动生成一些代码。
元函数
在C预处理器示例中,我们介绍了**元函数**的概念。总的来说,元函数是在我们所处的特定元编程领域中工作的函数。在C预处理器的情况下,我们显式地操作C源代码,所以它的元函数(宏)接收并操作C源代码。在C++模板元编程中,我们处理的是类型,所以元函数是处理类型的函数。C++模板也可以接受非类型参数,但使用不同类别的模板参数来做到通用是困难的。因此,只要有可能,我们将只使用类型参数。
template<typename T> struct identity { using type = T; };
identity
模板是一个代表恒等函数的元函数:它接受一个值(实际上是一个类型,因为我们处理的是类型)并返回它本身而不作修改。我们可以通过引用其成员类型type
来**“调用”**那个元函数。
using t = typename identity<int>::type; // t is int
当然,嵌套的元函数*调用*是可能的。
using t = typename identity<typename identity<int>::type>::type; //t is int
但是那个typename ::type
语法并不太好用。考虑一个更复杂的例子。
using t = typename add<typename add<std::integral_constant<int,1>,std::integral_constant<int,2>>::type, std::integral_constant<int,-2> >::type;
这个问题有几种可能的解决方案。
使用别名来代替元函数本身的结果。
自C++11以来,我们有了**模板别名**,一种参数化的typedef。我们可以使用它们来编写**用户侧元函数**。
template<typename LHS , typename RHS> using add = typename impl::add<LHS,RHS>::type;
其中add
是用户的**元函数**,而impl::add
是实际实现元函数的类模板。这允许我们以清晰的方式编写嵌套表达式。
using t = add<std::integral_constant<int,1>,add<std::integral_constant<int,-2>,std::integral_constant<int,-4>>;
构建表达式求值系统。
上述方法隐藏了对用户的实现细节。但是隐藏意味着这些用户侧元函数不是元函数,而是其结果的别名。这意味着我们不能在期望元函数的上下文中使用用户侧别名:**用户侧元函数不是一流函数**。相反,我们可以构建一个表达式求值系统,它接受一个表达式(带有其参数的模板)并对其进行求值,并说*“这是一个元函数吗?好的,那么我应该通过typename ::type
来获取它的结果”*。这种方法的好处是,可以自定义求值过程,并针对许多复杂情况进行设计。最简单的方法是在求值一个元函数之前先求值它的参数。我为Turbo做了这个,Boost.MPL.Lambda也采用了类似的方法。
//https://www.biicode.com/manu343726/manu343726/turbo_core/master #include "manu343726/turbo_core/turbo_core.hpp" using tml::placeholders::_1; using tml::placeholders::_2; //t is tml::Int<3> (std::integral_constant<int,3>) using t = tml::eval<tml::lambda<_1,_2 , tml::add<_1,_2>> , tml::Int<1>,tml::Int<2>>;
C++14变量模板:停止进行丑陋的模板元编程,使用自然的语法。
这种最后的方法自C++14以来可用,感谢**变量模板**。变量模板是一个由模板参数化的常量。典型示例是pi
常量,它知道所用类型的精度。
template<typename T> constexpr T pi = 3.141592654; float radious = 1.0f; float circle = pi<float>*pi<float>*radious;
变量模板是**由模板参数化的值**,而不是类型。因此,我们可以使用constexpr
函数而不是模板元函数来操作类型(想象一个变量模板充当类型的容器)。有关这种方法的示例,请参阅Boost.Hanna。
C++中的类Haskell语言。
由于我们使用C++类型系统,并将类型作为值用于我们的计算,所以tmp就像一种函数式编程语言;因为元函数没有副作用:**我们只能创建类型,而不能修改现有类型**。
就像在函数式语言中一样,tmp的支柱之一是**递归**。在这种情况下,是**递归模板实例化**(记住这个名字)。template<typename T> struct throw_stars { using type = T; }; template<typename T> struct throw_stars<T*> { using type = typename throw_stars<T>::type; };
我认为经典的阶乘/斐波那契元函数示例太无聊了。这里有一个更有趣的东西:throw_stars
模板是一个元函数,它接受一个类型并丢弃所有*“星号”*。
using t = typename throw_stars<int********>::type; //t is int
模板特化充当递归情况,而主模板充当基本情况。请注意,C++模板特化是如何表现得像模式匹配的。另一个例子可能是遍历C++11的变长参数包。
template<typename HEAD , typename... TAIL> struct last { using type = typename last<TAIL...>::type; }; template<typename T> struct last<T> { using type = T; }; using t = typename last<int,char,bool,double>::type; //t is double
这是函数式语言中常见的列表遍历的head:tail
方法的绝佳示例。
摘要
在C++模板元编程的第一个方法中,我们已经看到:
- 元编程是编写代码来生成代码的过程,也就是说,自动化代码生成。
- C++模板元编程使用模板系统来生成类型,并在过程中生成代码:我们使用模板生成类型,并且我们实际使用这些类型进行计算或生成所需的代码。
- 元编程的基本单元是 **元函数**,就像在普通编程中函数是基本单元一样。元函数操作它们特定元编程域中的实体。在C++模板元编程中,这些实体是类型,元函数通过模板表示。
- 模板元编程就像一种嵌入到C++本身的函数式语言。这种“*语言*”没有副作用(我们不能修改现有类型,只能创建新类型),所以我们使用与Haskell或F#等函数式编程语言相同的模式。
现在我们对C++模板元编程有了很好的概述,但在深入研究它之前,我们需要一些C++知识。下次我们将深入学习C++模板:模板参数、模板特化、SFINAE等;以确保我们都拥有并理解进行现代C++的适当元编程所需的工具。*作者:Manu Sánchez。*