C++语言中最有魅力的地方可能就是模板元编程了,它不但为框架,库的开发者提供了强有力的手段,而且通常是零成本的抽象。在C++中,能够与编译器交互的工具是类型,我们可以通过强大的类型系统在编译期完成许多工作,并且能够利用类型系统生成高效的最终代码。
如果将类型视作对象,那么计算对象的常常被称为函数,编译时的函数也被称为元函数,传统上使用模板来表达它,模板参数即函数的输入,模板的类成员即函数的输出。模板参数既可以是参数类型,也可以是非类型的参数,甚至可以是模板类(元函数)本身。一旦将元函数作为它的输入或输出,那么该函数可视作高阶函数,有了这一丰富的组合手段,就能够在编译时完成任意复杂的程序。
运行时计算与编译时计算相比,前者能够处理的元素有用户输入的数据,动态创建的对象以及函数;后者能够处理的元素有常量,类型,对象以及元函数。初看后者能够处理的元素比前者丰富,但在编译计算时是不允许有副作用存在的,这意味着同样的编程任务,编译时的难度要比运行时大得多。好在可以借助成熟的函数式编程规范来解决,也可以使用静态断言来判断。
5.2.1、数值计算
考虑编译时计算斐波那契数列的第N项的情况,如果使用模板类来表达,它将以非类型参数N作为输入,并且以成员变量value作为输出。
template<size_t N>
struct Fibonacci
{
static constexpr size_t value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value;
};
template<>
struct Fibonacci<1>
{
static constexpr size_t value = 1;
};
template<>
struct Fibonacci<0>
{
static constexpr size_t value = 0;
};
模板函数与普通函数不同之处在于,包裹函数的圆括号变成了尖括号,并且需要显示的指定返回值,返回值为值时,通常使用成员value表示。返回值为类型时,通常使用type成员表示。
由于在编译时不允许修改输入的非类型参数,也就意味着无法简单通过for循环对迭代的变量进行修改并求值。模板元编程通过递归的方式解决这类问题,该方式只需要在输入参数的基础上进行计算而无需修改原值。
递归的边界是通过特化的形式表达的,当输入为0或者为1时,停止递归求值,直接返回边界的结果。这个例子非常简单,但它蕴含着元编程的基本思想:使用递归代替迭代,使用特化代替分支,即“图形完备”。
5.2.2、类型计算
模板元编程另一个强大的地方在于其类型计算,这一点在库开发和框架开发中的应用也很广泛。
考虑多维数组的场景,标准库中的array相对原生数组无额外开销,并且提供了一些友好的操作接口。然而在使用多维数组的时候,比起原生数组要繁琐得多。
使用模板元编程可以为用户封装一层友好的接口。Array元函数将接受一个类型参数,和至少一个和非类型参数的维度信息,返回的结果为多维数组类型。
template<class T,size_t n,size_t ...is> //主模板
struct Array {
using type = array<typename Array<T, is...>::type, n>;
};
template<class T,size_t i> //边界情况,一维数组
struct Array<T,i> {
using type = array<T, i>;
};
using twoWayArray = array<array<int, 2>, 2>;
static_assert(same_as < Array<int, 2, 2>::type, twoWayArray>); //true
在一般情况下,递归的调用元函数Array,并减少一层维度,直到只有一层维度的边界情况,就是最简单的一维array,它将在层次递归中被最终合并为多维数组。
Array<int,5,4,3>::type
=> Array<Array<int,4,3>::type,5>::type
=> Array<Array<Array<int,3>::type,4>::type,5>::type
=> Array<Array<array<int,3>,4>::type,5>::type
=> Array>array<array<int,3>,4>,5>::type
=> array<array<array<int,3>,4>,5>
5.3、TypeList
在上文的例子中我们初步探究了元编程的能力,然而对于编译时构造大型程序而言,还需要一些工具集,例如在过程式编程中涉及拥有复合数据结构的数组,结构体等。
元编程中最基本的工具便是TypeList,它是类型的列表,也就是利用变参模板类来存储类型信息。
template<class ...Args>
struct TypeList{};
using List = TypeList<int,double>;
在标准库中有类似的工具integer_sequence,它只能存储数字列表,可视作TypeList的特殊形态,因为在C++元编程中,值与类型之间可以相互映射:用类型携带值,值承载于类型,即值可以映射成类型从而存储到TypeList中。
using One = std::integral_constant<int, 1>; //值承载于类型
constexpr auto one = One::value; //类型转换成值
using Two = std::integral_constant<int, 2>; //值承载于类型
constexpr auto two = Two::value; //类型转换成值
using List = TypeList<One,Two>;
上述代码中,值1映射到独一无二的类型One上,而不是它本身的类型int,因为我们无法从值域相当大的int类型映射到1,而One类型可以直接映射回值。标准库中的一个例子是bool类型与值的映射分别为true_type,false_type,它们分别表达true,false。
类型是程序员与编译器交互的桥梁,模板元编程的本质即类型的计算,可见元编程的重要性。
Comments NOTHING