打印

C++模板元编程(C++ template metaprogramming)

[复制链接]
楼主: elecintop
手机看帖
扫描二维码
随时随地手机跟帖
61
elecintop|  楼主 | 2015-3-24 21:25 | 只看该作者 回帖奖励 |倒序浏览
这里可以看到基类的作用:提供统一的接口,让 operator- 和 operator* 可以写成统一的模板形式。

使用特权

评论回复
62
elecintop|  楼主 | 2015-3-24 21:25 | 只看该作者
7. 特性,策略,标签

利用迭代器,我们可以实现很多通用算法,迭代器在容器与算法之间搭建了一座桥梁。求和函数模板如下:
#include <iostream> // std::cout
#include <vector>

template<typename iter>
typename iter::value_type mysum(iter begin, iter end) {
    typename iter::value_type sum(0);
    for(iter i=begin; i!=end; ++i) sum += *i;
    return sum;
}

int main() {
    std::vector<int> v;
    for(int i = 0; i<100; ++i) v.push_back(i);
    std::cout << mysum(v.begin(), v.end()) << '\n';
    std::cin.get(); return 0;
}

4950

使用特权

评论回复
63
elecintop|  楼主 | 2015-3-24 21:26 | 只看该作者
我们想让 mysum() 对指针参数也能工作,毕竟迭代器就是模拟指针,但指针没有嵌套类型 value_type,可以定义 mysum() 对指针类型的特例,但更好的办法是在函数参数和 value_type 之间多加一层 — 特性(traits)(参考了文献[1]第72页,特性详见文献[1] 12.1):
// 特性,traits
template<typename iter>
class mytraits{
public: typedef typename iter::value_type value_type;
};
template<typename T>
class mytraits<T*>{
public: typedef T value_type;
};

template<typename iter>
typename mytraits<iter>::value_type mysum(iter begin, iter end) {
    typename mytraits<iter>::value_type sum(0);
    for(iter i=begin; i!=end; ++i) sum += *i;
    return sum;
}

int main() {
    int v[4] = {1,2,3,4};
    std::cout << mysum(v, v+4) << '\n';
    std::cin.get(); return 0;
}

使用特权

评论回复
64
elecintop|  楼主 | 2015-3-24 21:26 | 只看该作者
其实,C++ 标准定义了类似的 traits:std::iterator_trait(另一个经典例子是 std::numeric_limits) 。特性对类型的信息(如 value_type、 reference)进行包装,使得上层代码可以以统一的接口访问这些信息。C++ 模板元编程会涉及大量的类型计算,很多时候要提取类型的信息(typedef、 常量值等),如果这些类型的信息的访问方式不一致(如上面的迭代器和指针),我们将不得不定义特例,这会导致大量重复代码的出现(另一种代码膨胀),而通过加一层特性可以很好的解决这一问题。另外,特性不仅可以对类型的信息进行包装,还可以提供更多信息,当然,因为加了一层,也带来复杂性。特性是一种提供元信息的手段。

使用特权

评论回复
65
elecintop|  楼主 | 2015-3-24 21:26 | 只看该作者
策略(policy)一般是一个类模板,典型的策略是 STL 容器(如 std::vector<>,完整声明是template<class T, class Alloc=allocator<T>> class vector;)的分配器(这个参数有默认参数,即默认存储策略),策略类将模板的经常变化的那一部分子功能块集中起来作为模板参数,这样模板便可以更为通用,这和特性的思想是类似的(详见文献[1] 12.3)。

使用特权

评论回复
66
elecintop|  楼主 | 2015-3-24 21:27 | 只看该作者
标签(tag)一般是一个空类,其作用是作为一个独一无二的类型名字用于标记一些东西,典型的例子是 STL 迭代器的五种类型的名字(input_iterator_tag, output_iterator_tag, forward_iterator_tag, bidirectional_iterator_tag, random_access_iterator_tag),std::vector<int>::iterator::iterator_category 就是 random_access_iterator_tag,可以用第1节判断类型是否等价的模板检测这一点:
#include <iostream>
#include <vector>

template<typename T1, typename T2> // 通例,返回 false
class theSameType       { public: enum { ret = false }; };
template<typename T>               // 特例,两类型相同时返回 true
class theSameType<T, T> { public: enum { ret = true }; };

int main(){
    std::cout << theSameType< std::vector<int>::iterator::iterator_category,
                              std::random_access_iterator_tag >::ret << '\n';
    std::cin.get(); return 0;
}

使用特权

评论回复
67
elecintop|  楼主 | 2015-3-24 21:27 | 只看该作者
有了这样的判断,还可以根据判断结果做更复杂的元编程逻辑(如一个算法以迭代器为参数,根据迭代器标签进行特例化以对某种迭代器特殊处理)。标签还可以用来分辨函数重载,第5节中就用到了这样的标签(recursion)(标签详见文献[1] 12.1)。

使用特权

评论回复
68
elecintop|  楼主 | 2015-3-24 21:28 | 只看该作者
8. 更多类型计算

在第1节我们讲类型等价的时候,已经见到了一个可以判断两个类型是否等价的模板,这一节我们给出更多例子,下面是判断一个类型是否可以隐式转换到另一个类型的模板(参考了文献[6] Static interface checking):
#include <iostream> // std::cout

// whether T could be converted to U
template<class T, class U>
class ConversionTo {
    typedef char Type1[1]; // 两种 sizeof 不同的类型
    typedef char Type2[2];
    static Type1& Test( U ); // 较下面的函数,因为参数取值范围小,优先匹配
    static Type2& Test(...); // 变长参数函数,可以匹配任何数量任何类型参数
    static T MakeT(); // 返回类型 T,用这个函数而不用 T() 因为 T 可能没有默认构造函数
public:
    enum { ret = sizeof(Test(MakeT()))==sizeof(Type1) }; // 可以转换时调用返回 Type1 的 Test()
};

int main() {
    std::cout << ConversionTo<int, double>::ret << '\n';
    std::cout << ConversionTo<float, int*>::ret << '\n';
    std::cout << ConversionTo<const int&, int&>::ret << '\n';
    std::cin.get(); return 0;
}

1
0
0

使用特权

评论回复
69
elecintop|  楼主 | 2015-3-24 21:29 | 只看该作者
下面这个例子检查某个类型是否含有某个嵌套类型定义(参考了文献[4] Substitution failure is not an erro (SFINAE)),这个例子是个内省(反射的一种):
#include <iostream>
#include <vector>

// thanks to Substitution failure is not an erro (SFINAE)
template<typename T>
struct has_typedef_value_type {
    typedef char Type1[1];
    typedef char Type2[2];
    template<typename C> static Type1& test(typename C::value_type*);
    template<typename> static Type2& test(...);
public:
    static const bool ret = sizeof(test<T>(0)) == sizeof(Type1); // 0 == NULL
};

struct foo { typedef float lalala; };

int main() {
    std::cout << has_typedef_value_type<std::vector<int>>::ret << '\n';
    std::cout << has_typedef_value_type<foo>::ret << '\n';
    std::cin.get(); return 0;
}

使用特权

评论回复
70
elecintop|  楼主 | 2015-3-24 21:29 | 只看该作者
这个例子是有缺陷的,因为不存在引用的指针,所以不用用来检测引用类型定义。可以看到,因为只涉及类型推断,都是编译期的计算,不涉及任何可执行代码,所以类的成员函数根本不需要具体实现。

使用特权

评论回复
71
elecintop|  楼主 | 2015-3-24 21:29 | 只看该作者
9. 元容器

文献[1]第 13 章讲了元容器,所谓元容器,就是类似于 std::vector<> 那样的容器,不过它存储的是元数据 — 类型,有了元容器,我们就可以判断某个类型是否属于某个元容器之类的操作。

使用特权

评论回复
72
elecintop|  楼主 | 2015-3-24 21:30 | 只看该作者
在讲元容器之前,我们先来看看伪变长参数模板(文献[1] 12.4),一个可以存储小于某个数(例子中为 4 个)的任意个数,任意类型数据的元组(tuple)的例子如下(参考了文献[1] 第 225~227 页):
#include <iostream>

class null_type {}; // 标签类,标记参数列表末尾
template<typename T0, typename T1, typename T2, typename T3>
class type_shift_node {
public:
    typedef T0 data_type;
    typedef type_shift_node<T1, T2, T3, null_type> next_type; // 参数移位了
    static const int num = next_type::num + 1; // 非 null_type 模板参数个数
    data_type data; // 本节点数据
    next_type next; // 后续所有节点数据
    type_shift_node() :data(), next() { } // 构造函数
    type_shift_node(T0 const& d0, T1 const& d1, T2 const& d2, T3 const& d3)
        :data(d0), next(d1, d2, d3, null_type()) { } // next 参数也移位了
};
template<typename T0> // 特例,递归终止
class type_shift_node<T0, null_type, null_type, null_type> {
public:
    typedef T0 data_type;
    static const int num = 1;
    data_type data; // 本节点数据
    type_shift_node() :data(), next() { } // 构造函数
    type_shift_node(T0 const& d0, null_type, null_type, null_type) : data(d0) { }
};
// 元组类模板,默认参数 + 嵌套递归
template<typename T0, typename T1=null_type, typename T2=null_type,
         typename T3=null_type>
class my_tuple {
public:
    typedef type_shift_node<T0, T1, T2, T3> tuple_type;
    static const int num = tuple_type::num;
    tuple_type t;
    my_tuple(T0 const& d0=T0(),T1 const& d1=T1(),T2 const& d2=T2(),T3 const& d3=T3())
        : t(d0, d1, d2, d3) { } // 构造函数,默认参数
};

// 为方便访问元组数据,定义 get<unsigned>(tuple) 函数模板
template<unsigned i, typename T0, typename T1, typename T2, typename T3>
class type_shift_node_traits {
public:
    typedef typename
        type_shift_node_traits<i-1,T0,T1,T2,T3>::node_type::next_type node_type;
    typedef typename node_type::data_type data_type;
    static node_type& get_node(type_shift_node<T0,T1,T2,T3>& node)
    { return type_shift_node_traits<i-1,T0,T1,T2,T3>::get_node(node).next; }
};
template<typename T0, typename T1, typename T2, typename T3>
class type_shift_node_traits<0, T0, T1, T2, T3> {
public:
    typedef typename type_shift_node<T0,T1,T2,T3> node_type;
    typedef typename node_type::data_type data_type;
    static node_type& get_node(type_shift_node<T0,T1,T2,T3>& node)
    { return node; }
};
template<unsigned i, typename T0, typename T1, typename T2, typename T3>
typename type_shift_node_traits<i,T0,T1,T2,T3>::data_type
get(my_tuple<T0,T1,T2,T3>& tup) {
    return type_shift_node_traits<i,T0,T1,T2,T3>::get_node(tup.t).data;
}

int main(){
    typedef my_tuple<int, char, float> tuple3;
    tuple3 t3(10, 'm', 1.2f);
    std::cout << t3.t.data << ' '
              << t3.t.next.data << ' '
              << t3.t.next.next.data << '\n';
    std::cout << tuple3::num << '\n';
    std::cout << get<2>(t3) << '\n'; // 从 0 开始,不要出现 3,否则将出现不可理解的编译错误
    std::cin.get(); return 0;
}

10 m 1.2
3
1.2

使用特权

评论回复
73
elecintop|  楼主 | 2015-3-24 21:30 | 只看该作者
利用和上面例子类似的模板参数移位递归的原理,我们可以构造一个存储“类型”的元组,即元容器,其代码如下(和文献[1]第 237 页的例子不同):
#include <iostream>

// 元容器
template<typename T0=void, typename T1=void, typename T2=void, typename T3=void>
class meta_container {
public:
    typedef T0 type;
    typedef meta_container<T1, T2, T3, void> next_node; // 参数移位了
    static const int size = next_node::size + 1; // 非 null_type 模板参数个数
};
template<> // 特例,递归终止
class meta_container<void, void, void, void> {
public:
    typedef void type;
    static const int size = 0;
};

// 访问元容器中的数据
template<typename C, unsigned i>
class get {
public:
    static_assert(i<C::size, "get<C,i>: index exceed num"); // C++11 引入静态断言
    typedef typename get<C,i-1>::c_type::next_node c_type;
    typedef typename c_type::type ret_type;
};
template<typename C>
class get<C, 0> {
public:
    static_assert(0<C::size, "get<C,i>: index exceed num"); // C++11 引入静态断言
    typedef C c_type;
    typedef typename c_type::type ret_type;
};

// 在元容器中查找某个类型,找到返回索引,找不到返回 -1
template<typename T1, typename T2> class same_type { public: enum { ret = false }; };
template<typename T> class same_type<T, T> { public: enum { ret = true }; };

template<bool c, typename Then, typename Else> class IF_ { };
template<typename Then, typename Else>
class IF_<true, Then, Else> { public: typedef Then reType; };
template<typename Then, typename Else>
class IF_<false, Then, Else> { public: typedef Else reType; };

template<typename C, typename T>
class find {
    template<int i> class number { public: static const int ret = i; };
    template<typename C, typename T, int i>
    class find_i {
    public:
        static const int ret = IF_< same_type<get<C,i>::ret_type, T>::ret,
            number<i>, find_i<C,T,i-1> >::reType::ret;
    };
    template<typename C, typename T>
    class find_i<C, T, -1> {
    public:
        static const int ret = -1;
    };
public:
    static const int ret = find_i<C, T, C::size-1>::ret;
};

int main(){
    typedef meta_container<int, int&, const int> mc;
    int a = 9999;
    get<mc, 1>::ret_type aref = a;
    std::cout << mc::size << '\n';
    std::cout << aref << '\n';
    std::cout << find<mc, const int>::ret << '\n';
    std::cout << find<mc, float>::ret << '\n';
    std::cin.get(); return 0;
}

3
9999
2
-1

使用特权

评论回复
74
elecintop|  楼主 | 2015-3-24 21:31 | 只看该作者
上面例子已经实现了存储类型的元容器,和元容器上的查找算法,但还有一个小问题,就是它不能处理模板,编译器对模板的操纵能力远不如对类型的操纵能力强(提示:类模板实例是类型),我们可以一种间接方式实现存储“模板元素”,即用模板的一个代表实例(如全用 int 为参数的实例)来代表这个模板,这样对任意模板实例,只需判断其模板的代表实例是否在容器中即可,这需要进行类型过滤:对任意模板的实例将其替换为指定模板参数的代表实例,类型过滤实例代码如下(参考了文献[1]第 241 页):


// 类型过滤,meta_filter 使用时只用一个参数,设置四个模板参数是因为,模板通例的参数列表
// 必须能够包含特例参数列表,后面三个参数设置默认值为 void 或标签模板
template<typename T> class dummy_template_1 {};
template<typename T0, typename T1> class dummy_template_2 {};
template<typename T0, typename T1 = void,
    template<typename> class tmp_1 = dummy_template_1,
    template<typename, typename> class tmp_2 = dummy_template_2>
class meta_filter { // 通例,不改变类型
public:
    typedef T0 ret_type;
};
                    // 匹配任何带有一个类型参数模板的实例,将模板实例替换为代表实例
template<template<typename> class tmp_1, typename T>
class meta_filter<tmp_1<T>, void, dummy_template_1, dummy_template_2> {
public:
    typedef tmp_1<int> ret_type;
};
                    // 匹配任何带有两个类型参数模板的实例,将模板实例替换为代表实例
template<template<typename, typename> class tmp_2, typename T0, typename T1>
class meta_filter<tmp_2<T0, T1>, void, dummy_template_1, dummy_template_2> {
public:
    typedef tmp_2<int, int> ret_type;
};

使用特权

评论回复
75
elecintop|  楼主 | 2015-3-24 21:32 | 只看该作者
现在,只需将上面元容器和元容器查找函数修改为:对模板实例将其换为代表实例,即修改 meta_container<> 通例中“typedef T0 type;”语句为“typedef typename meta_filter<T0>::ret_type type;”,修改 find<> 的最后一行中“T”为“typename meta_filter<T>::ret_type”。修改后,下面代码的执行结果是:
template<typename, typename> class my_tmp_2;

// 自动将 my_tmp_2<float, int> 过滤为 my_tmp_2<int, int>
typedef meta_container<int, float, my_tmp_2<float, int>> mc2;
// 自动将 my_tmp_2<char, double> 过滤为 my_tmp_2<int, int>
std::cout << find<mc2, my_tmp_2<char, double>>::ret << '\n'; // 输出 2

使用特权

评论回复
76
elecintop|  楼主 | 2015-3-24 21:33 | 只看该作者
10. 总结

博文比较长,总结一下所涉及的东西:

1、C++ 模板包括函数模板和类模板,模板参数形式有:类型、模板型、非类型(整型、指针);
2、模板的特例化分完全特例化和部分特例化,实例将匹配参数集合最小的特例;
3、用实例参数替换模板形式参数称为实例化,实例化的结果是产生具体类型(类模板)或函数(函数模板),同一模板实参完全等价将产生等价的实例类型或函数;

使用特权

评论回复
77
elecintop|  楼主 | 2015-3-24 21:34 | 只看该作者
4、模板一般在头文件中定义,可能被包含多次,编译和链接时会消除等价模板实例;
5、template、typename、this 关键字用来消除歧义,避免编译错误或产生不符预期的结果;
6、C++11 对模板引入了新特性:“>>”、函数模板也可以有默认参数、变长模板参数、外部模板实例(extern),并弃用 export template;

使用特权

评论回复
78
elecintop|  楼主 | 2015-3-24 21:34 | 只看该作者
7、C++ 模板是图灵完备的,模板编程是函数编程风格,特点是:没有可变的存储、递归,以“<>”为输入,typedef 或静态常量为输出;
8、编译期数值计算虽然实际意义不大,但可以很好证明 C++ 模板的能力,可以用模板实现类似普通程序中的 if 和 while 语句;
9、一个实际应用是循环展开,虽然编译器可以自动循环展开,但我们可以让这一切更可控;

使用特权

评论回复
79
elecintop|  楼主 | 2015-3-24 21:35 | 只看该作者
10、C++ 模板编程的两个问题是:难调试,会产生冗长且难以阅读的编译错误信息、代码膨胀(源代码膨胀、二进制对象文件膨胀),改进的方法是:增加一些检查代码,让编译器及时报错,使用特性、策略等让模板更通用,可能的话合并一些模板实例(如将代码提出去做成单独模板);
11、表达式模板和向量计算是另一个可加速程序的例子,它们将计算表达式编码到类型,这是通过模板嵌套参数实现的;
12、特性,策略,标签是模板编程常用技巧,它们可以是模板变得更加通用;

使用特权

评论回复
80
elecintop|  楼主 | 2015-3-24 21:35 | 只看该作者
13、模板甚至可以获得类型的内部信息(是否有某个 typedef),这是反射中的内省,C++ 在语言层面对反射支持很少(typeid),这不利于模板元编程;
14、可以用递归实现伪变长参数模板,C++11 变长参数模板背后的原理也是模板递归;
15、元容器存储元信息(如类型)、类型过滤过滤某些类型,它们是元编程的高级特性。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则