模板元编程

来源:互联网 发布:安川伺服软件下载 编辑:程序博客网 时间:2024/06/07 07:15

《C++11/14高级编程:Boost程序库探秘》笔记

模板元编程简称元编程,本质上是泛型编程的一个子集,所以从广义上说,所有使用template的泛型编程都可以称作元程序——因为泛型代码并不是真正可编译执行的代码。
模板元编程的运行是在编译期,它把编译器变成了元程序的解释器。

语法元素

模板元编程产生的元程序是在编译期执行的程序,操作的对象也不是普通的变量,因此不能使用运行时的C++关键字(如if、else、for),可用的语法元素相当有限,最常用的几种:

  • enum、static,用来定义编译期的整数常量
  • typedef、using(C++11/14),最重要的元编程关键字,用来定义元数据
  • template,模板元编程的“起点”,主要用于定义元函数
  • “::”,域运算符,用于解析类型作用域获取计算结果(元数据)

元数据

元编程可操作的数据称为“元数据”(meta data),也就是C++编译器在编译期可操作的数据,它是模板元编程的基础。元数据都是不可变的,不能就地修改,最常见的是整数和C++的类型(type)。


元函数

元函数是模板元编程中用于操作处理元数据的“构件”,可以在编译期被“调用”,功能形式类似运行时的函数,但形式上却是一个模板类。

  • 函数参数列表圆括号变成了模板列表声明的尖括号
  • 函数的形参变成了模板参数(即元数据),并且要使用关键字typedname修饰
  • 因为不能使用运行时关键字,所以元函数不能像普通函数那样使用return返回结果,而需要在元函数内部用typedef/using定义一个名为type的类型或名为value的值作为返回
  • 元函数最后以分号结束
template<int N,int M>struct meta_func{    static const int value = N + M;};cout << meta_func<10, 10>::value << endl;int i = 10, j = 10;meta_func<i, j>::value; // error,i,j为运行时变量

元函数可以使用typedef/using关键字返回多个返回值,并且没有顺序关系,能够用”::”获取。


元函数转发

使用public继承,模板参数传递给父类完成元函数的“调用”,子类会自动获得父类的::type定义,同时也完成了元函数的返回。

struct select1st{    typedef T1 type;};template<typename T1,typename T2>struct forward :select1st<T2,T1>    //默认public继承{};

等价于:

template<typename T1,typename T2>struct forward{    typedef typename select1st<T2, T1>::type type;  //调用元函数计算};

mpl工具库

mpl是专门为模板元编程创建的工具库,提供了大量高质量高效的元编程工具,包括:

  • 基本的数据类型:整数、pair、void等运行时类型的对应物
  • 基本的数据运算:以元函数形式提供的算术运算、逻辑运算等运算功能
  • 程序流程控制:以元函数形式提供的分支流程处理功能
  • 容器:模仿STL风格的存储元数据(类型)的编译期容器
  • 视图:容器的适配器,可以简化容器的操作
  • 迭代器:模仿STL风格应用于容器的编译期迭代器
  • 算法:模仿STL风格应用于容器和迭代器的编译期算法
  • 高阶元数据:类似于函数对象的元编程构件
  • bind/lambda表达式:编译期的lambda表达式,功能强大灵活
  • 调试支持:提供了编译期的断言功能
  • 辅助宏:各种配置宏和traits宏

mpl由头文件组成,所有组件位于名字空间boost::mpl,源代码在目录<boost/mpl>下,文件名和组件名一致,不存在统一的头文件,所以要手工添加包含的头文件。


整数类型

虽然整数本身就是元数据,在编译期可以计算,但直接操作整数对于元编程并不太方便,因为元编程更大的作用是类型计算,整数不是类型,为此,mpl库提供了数值包装器概念,把整数(int、long、bool等类型)包装为值元函数。
mpl库里的数值包装器元函数共有六个,分别包装了不同的整数类型(没有short):

char_<N>            :包装char类型,值为Nint_<N>             :包装int类型,值为Nlong_<N>            :包装long类型,值为Nsize_t<N>           :包装size_t类型,值为Nintergral_c<T,N>    :包装类型为T的整数类型,值为Nbool_<N>            :包装bool类型,值为N

这些元函数都有基本相同的形式,类摘要如下:

struct integral_wrapper //整数包装器{    BOOST_STATIC_CONSTANT(T, value = N);    //包装整数值N    typedef integral_c_tag  tag;            //类型标记    typedef T               type;           //返回自身    typedef T               value_type;     //整数类型    operator T() const { return this->value; }  //转型操作    //bool没有下列两个元数据    typedef some_define     next;           //++N    typedef some_define     prior;          //--N};

用法也基本相同,用::type返回自身,用::value返回被包含的整数值,对于非bool类型,提供了 next和prior两个内部类型定义,可以获得类似operator++和operator–效果,同时重载了转型操作,使得它们的实例可以在运行时隐式转换为被包装的整数。

#include <type_traits>#include <boost/mpl/int.hpp>#include <boost/mpl/integral_c.hpp>#include <boost/mpl/prior.hpp>using namespace boost::mpl;typedef int_<2> i2;typedef integral_c<short, 2> s2;int main(){    assert(i2::value == 2);    assert(i2::value == s2::value);    //is_same<T1,T2>::value比较T1T2两个的类型是否一致    assert((std::is_same<i2::type, i2>::value));    assert((std::is_same<s2::value_type, short>::value));    assert(i2::next::value == 3);           //内部成员获取递增值    assert(prior<s2>::type::value == 1);    //使用元函数获取递减值    i2 two1;    //声明两个包装器实例    s2 two2;    //值均为常量2    int i = two1 + two2;        //隐式类型转换为int参与运行时计算    assert(i == int_<4>());     //与int_元函数比较,使用()创建临时对象    return 0;}

mpl库提供了同C++内建的整数运算一样的整数类型运算:

  • 递增递减运算:位于头文件<boost/mpl/next_prior.hpp>,包括next和prior两个元函数,不支持bool_。
  • 算术运算:位于头文件<boost/mpl/arithmetic.hpp>,包括加减乘除、取模、取负值,支持使用多个参数,元函数有plus,minus,negate等。
  • 比较运算:位于头文件<boost/mpl/comparison.hpp>,包括小于,大于,等于和不等于六种,元函数有less,greater,equal_to等。
  • 位运算:位于头文件<boost/mpl/bitwise.hpp>,包括与、或、异或、移位等,元函数有bitand_、bitor_等。
  • 逻辑运算:位于头文件<boost/mpl/logical.hpp>,包括与、或、非三种逻辑运算,元函数有and_、or_和not_。
  • 其他运算:包括最大最小值min/max、整数的大小sizeof_等。

流程控制

mpl提供了四个元函数用于分支结构,类似于运行时的if-else语句

  • if_和if_c
    近似于 ?: 操作符
template<bool C,typename T1,typename T2>struct if_c{    typedef T1 type;};template<typename T1,typename T2>struct if_c<false, T1, T2>          //if_c对false模板偏特化{    typedef T2 type;};template<typename C,typename T1, typename T2>struct if_{    typedef if_c<C::value, T1, T2> almost_type_t;    typedef typename almost_type_t::type type;};

if_c和if_ 的条件不同,前者是bool类型,后者是一个有::value返回的值元函数(不一定是bool_)。

typedef if_c<true, int, long>::type mdata1;assert((is_same<mdata1, int>::value));typedef if_<boost::mpl::false_, float, double>::type mdata2;assert((is_same<mdata2, double>::value));

if_c 和if_ 不能有选择地忽略不需要计算的数据(缓式评估),必须计算所有的元数据,增加了计算时间。

  • eval_if和eval_if_c
    这两者的出现是为了解决上述if_c和if_ 的缺陷:缓式评估问题
template<bool C,typename F1,typename F2>struct eval_if_c    : if_c<C,F1,F2>::type   //元函数转发{};template<typename C,typename F1, typename F2>struct eval_if    : if_<C,F1,F2>::type{};

使用时需要注意,后两个模板参数必须是元函数。

typedef eval_if_c<true, identity<int>, identity<long>>::type mdata1;assert((is_same<mdata1, int>::value));typedef eval_if<is_integral<mdata1>, identity<float>, identity<double>>::type mdata2;assert((is_same<mdata2, float>::value));

mpl容器

mpl容器与标准库的容器相似,但容纳的元素都是元数据(也就是类型),没有对元素的可拷贝可赋值要求。mpl容器没有成员函数,自身不能操纵容器内元素,只能通过外部元函数处理。
mpl序列容器包括以下内容:

  • list:不同于std::list,它是一个单向链表,只能在序列的前端操作元素可以容纳无限个元素
  • vector:跟std::vector一样有随机访问的功能,但它又有std::list的特性,可以在两端操作元素,容量有限,缺省最多能容纳20个元数据
  • deque:类似std::deque,可以在两端操作元素。

mpl关联容器缺省最多能容纳20个元素,包含以下两种:

  • set:类似std::multiset,是一个允许重复的元数据集合
  • map:类似std::multimap,是一个允许重复的元数据映射关系集合

除了上述五个基本容器,mpl库还针对整数类型提供了一些特别的整数容器:

  • range_c<T,N,M> :不可修改的包含[N,M]区间内整数的vector容器
  • list_c<T,…> :包含类型为T的若干整数的list容器
  • vector_c<T,…> :包含类型为T的若干整数的vector容器
  • set_c<T,…> : 包含类型为T的若干整数的set容器
  • string :专门存储char字符的容器,类似std::string,可以在编译期处理的字符串

判断一个类型是否是mpl容器可以用元函数is_sequence。

mpl容器的相关元函数
1) 容器容量操作。

  • empty<C>
  • size<C>

2)元素访问操作。

  • front<C>
  • back<C>
  • at<C,k> :对于序列容器,返回第k个位置上的元素,对于关联容器,返回键为k的元素
  • at_c<C,n> :仅用于序列容器, 返回第n个位置上的元素(n是long型整数)

3)迭代器操作

  • begin<C>
  • end<C>

4)元素变动操作

  • push_front<C,t>
  • push_back<C,t>
  • pop_front<C>
  • pop_back<C>
  • clear<C>
  • erase<C,pos>/erase<C,f,l>
  • insert<C,pos,t>/insert<C,t>

容器是元数据,元数据是不可变的,所以变动操作都必须返回一个新的容器,原容器不会变化

5)关联容器特有操作

  • has_key<C,k>
  • erase_key<C,k>

mpl迭代器

mpl所有的迭代器都是只读,可分为三类:

  1. 前向迭代器:可以使用元函数next前进
  2. 双向迭代器:在前向迭代器的基础上增加使用元函数prior后退,即可逆向遍历
  3. 随机访问迭代器:在双向迭代器的基础上增加迭代器的距离运算

提供的迭代器元函数包括以下几个:

  • deref<I> : 解引用迭代器,返回元数据
  • next<I> :前进迭代器
  • prior<I> :后退迭代器
  • advance<I,n> :迭代器移动n个位置,对于双向迭代器n可以是负值
  • distance<I1,I2> :返回两个迭代器之间的距离
  • iterator_category<I> :获得迭代器的分类标志

mpl算法

1)插入器

  • back_inserter<C> :在容器后端插入元素,要求支持push_back
  • front_inserter<C> :在容器前端插入元素,要求支持push_front
  • inserter<S,Op> :通用的插入器,以初始状态S开始执行Op操作完成插入动作。
typedef mpl::vector<char,int,long> vec; //容纳三个元素typedef copy<vec,mpl:back_inserter<vec>>::type vec2; //插入自己,相当于双倍assert((size<vec2>::value == 6));typedef mpl::string<> str1;typedef copy<mpl::string<'time'>, //临时元数据    mpl::front_inserter<str1>>::type str2;cout << c_str<str2>::value;     //输出字符串emit

2)查询算法

  • find<C,t> :查找元素t,返回迭代器
  • contains<C,t> :值元函数,检查是否存在元素t
  • count<C,t> :值元函数,返回容器中元素t的个数
  • equal<C1,C2> :值元函数,比较两个容器是否等价,即元数据相同,容器可能是不同类型

3)变换算法
变换算法处理容器中的全部或部分元素,然后返回一个新的容器,带前缀”reverse_”的形式可以返回操作后的逆序容器。

  • copy<From,To> :拷贝一个容器里所有的元素
  • replace<From,Old,New,To> :把容器中的Old元素全部替换为New元素
  • remove<From,t,To> :移除容器中所有值为t的元素
  • reverse<From,To> :逆序拷贝容器里的所有元素

最后一个参数To可以省略不用,直接用::type返回变换后的新容器,如果使用,那么通常需要搭配插入器工作。

4)运行时算法
mpl中for_each算法可以遍历类型容器,工作在运行时,调用一个函数对象操作类型容器里类型对应的实例对象。
for_each算法是一个模板函数,有两个模板参数,第一个参数是mpl容器,必须显式指定,第二个是函数对象,它应具有模板成员函数operator(),能够处理mpl容器中的所有类型,否则编译错误。

struct mpl_func1{    template<typename T>    void operator()(T t)    {        cout << typeid(t).name() << endl;    }};struct mpl_func2{    template<typename T>    void operator()(T t)    {        if(is_same<typename tag<T>::type,integral_c_tag>::value)        {            cout << t << ',';        }    }};int main(){    typedef range_c<int, 0, 5> rc;    typedef mpl::vector<> vec;    typedef mpl::copy<rc,        mpl::back_inserter<vec>>::type vec2;    typedef push_front<vec2, float>::type vec3;    mpl::for_each<vec3>(mpl_func1());    mpl::for_each<vec3>(mpl_func2());    return 0;}

运行结果如下:

floatstruct boost::mpl::integral_c<int,0>struct boost::mpl::integral_c<int,1>struct boost::mpl::integral_c<int,2>struct boost::mpl::integral_c<int,3>struct boost::mpl::integral_c<int,4>0,1,2,3,4,

元编程断言

mpl库在头文件<boost/mpl/assert.hpp>中特意定义了几个元编程断言,能够在元程序发生错误时提供有用的信息。

  • 基本断言
    宏BOOST_MPL_ASSERT是最常用的一个静态断言,它使用一个返回bool_值元函数pred作为参数,断定pred::value为真,调用形式:
    BOOST_MPL_ASSERT((pred)); //必须两对圆括号
typedef mpl::vector<int,char> vec;BOOST_MPL_ASSERT((is_same<int,front<vec>::type>));  //注意不需要::valueBOOST_MPL_ASSERT((equal_to<size<vec>::type,int_<3>>));

第二个断言会在编译时报出形如”***pred::***”的错误,同时出现行号。

  • 否定断言
    BOOST_MPL_ASSERT_NOT
  • 关系断言
    BOOST_MPL_ASSERT_RELATION(x,rel,y)
    x,y是两个编译期整数(非包装器),rel是一个合法的C++关系操作符,如==、<。
    这个不需要两对圆括号
  • 定制消息断言
    BOOST_MPL_ASSERT_MSG(c,msg,types_)
    c:条件表达式,非元函数
    msg:自定义的消息,但不是一个C字符串,而是一个符合C++语法的标志符
    types_:类型列表

实例研究

实现一个动态加载动态链接库函数功能的实例研究
先编译一个简单的动态库:

//libtest.h#ifdef __cplusplusextern "C"{#endif    int so_func1(int x);    int so_func2(int x,int y);#ifdef __cplusplus}#endif
//libtest.cpp#include "libtest.h"int so_func1(int x){    return x*x;}int so_func2(int x,int y){    return x + y;}//g++-4.8 libtest.cpp -fPIC -shared -o libtest.so
1.泛型编程版本

简单地封装了一个so操作类

//gp_version.cpp#include <iostream>#include <stdexcept>    //runtime_error#include <dlfcn.h>      //UNIX动态链接头文件#include "libtest.h"class DlManager         //定义一个加载so函数的包装类{public:    DlManager(const char* name)     {        m_h = dlopen(name,RTLD_NOW);    //使用文件名加载so    }    ~DlManager()    {        dlclose(m_h);    }    template<typename FuncType>    FuncType load(const char* func_name)    {        FuncType pf = reinterpret_cast<FuncType>(dlsym(m_h,func_name)); //强制类型转换        if(!pf)        {            throw std::runtime_error(dlerror());        }        return pf;    }private:    typedef void* handle_t;     //句柄类型定义    handle_t m_h;               //so文件的句柄};typedef int (*Func1)(int);typedef int (*Func2)(int,int);int main(){    DlManager dm("./libtest.so");    Func1 f1 = dm.load<Func1>("so_func1");    Func2 f2 = dm.load<Func2>("so_func2");    std::cout << f1(10) << std::endl;    std::cout << f2(10,20) << std::endl;    return 0;}//g++-4.8 gp_version.cpp -ldl -o gp_version
2.元编程第1版

加载动态库这个过程,可以分解成两部分:一部分是编译期的数据,包括文件名、函数名以及函数指针类型,另一部分是运行时代码,包括加载so文件和获取so函数,泛型版本没有把这两者很好地解耦,而是将两者在运行时混合在一起了。
为了明确区分编译和运行时的数据,使用mpl把程序分为前端和后端两个部分,前端使用mpl定义编译期数据,后端使用前端数据实现运行时功能。

前端
前端数据有so文件名、接口函数名和函数指针类型,前两者可以使用mpl::string表述,后者本身就是元数据,为了实现函数名与函数指针类型的对应关系,可以使用mpl::map,最后用struct把这些数据封装成一个前端类。

后端
后端与泛型版本差不多,只不过操作的数据变成了前端的元数据,把后端实现为一个模板类,这样就可以使用不同的前端类支持不同的so加载功能。

//mp_version1.cpp#include <iostream>#include <boost/mpl/vector.hpp>#include <boost/mpl/string.hpp>#include <boost/mpl/map.hpp>#include <boost/mpl/at.hpp>#include <dlfcn.h>      //UNIX动态链接头文件#include "libtest.h"namespace mpl = boost::mpl;struct dl_front{    typedef mpl::string<'./','lib','test','.so'> so_name;    typedef int (*Func1)(int);    typedef int (*Func2)(int,int);    typedef mpl::string<'so_','func','1'> func1_name;    typedef mpl::string<'so_','func','2'> func2_name;    typedef mpl::map<        mpl::pair<func1_name,Func1>,        mpl::pair<func2_name,Func2>        > map_fun;};template<typename Front>class dl_back{private:    void* m_h = NULL;public:    dl_back()    {        m_h = dlopen(mpl::c_str<typename Front::so_name>::value,RTLD_NOW);    }    ~dl_back()    {        dlclose(m_h);    }    template<typename FuncName>    typename mpl::at<typename Front::map_fun,FuncName>::type    //返回类型    func_ptr()    {        typedef typename mpl::at<typename Front::map_fun,FuncName>::type result_type;        result_type pf = reinterpret_cast<result_type>(dlsym(m_h,mpl::c_str<FuncName>::value));        return pf;    }};int main(){    dl_back<dl_front> dl;    std::cout << dl.func_ptr<dl_front::func1_name>()(10) << std::endl;    std::cout << dl.func_ptr<dl_front::func2_name>()(10,20) << std::endl;    return 0;}//g++-4.8 mp_version1.cpp -ldl -o mp_version1

在调用成员函数func_ptr()时,除了显式写出前端定义的函数名外,还必须调用两次operator(),因为第一次调用只是获取了函数指针,第二次调用材质真正的so接口函数调用。

3.元编程第2版

第一版的元编程还可以增强成员函数func(),直接传递参数减少一次operator()调用。要减少一个operator()调用,就要求函数func()返回的是so函数指针的返回类型而不是函数指针类型,同时传递相应数量的参数,前者可以使用result_of结合mpl来自动推导函数的返回值类型,后者可以用可变模板参数列表解决。
改进后func()代码:

template<typename FuncName,typename ... Args>   //可变参数模板    typename boost::result_of<                      //使用result_of推导函数的返回值        typename mpl::at<typename Front::map_fun,FuncName>::type(Args...)        >::type    func(Args const& ...args)    {        typedef typename mpl::at<typename Front::map_fun,FuncName>::type func_type; //得到函数指针类型        func_type pf = reinterpret_cast<func_type>(dlsym(m_h,mpl::c_str<FuncName>::value)); //获取函数指针        return pf(args...); //直接调用函数指针    }

也可以直接使用第一版写好的func_ptr()函数直接返回函数指针来调用:

template<typename FuncName,typename ... Args>   //可变参数模板    typename boost::function_traits<            //改用function_traits推导类型        typename boost::remove_pointer<         //需要先移除类型里的指针            typename mpl::at<typename Front::map_fun,FuncName>::type            >::type                     >::result_type                  //获取函数的返回值    func_V(Args const& ... args)    {        return func_ptr<FuncName>()(args...);    }

完整代码:

//mp_version2.cpp#include <iostream>#include <boost/mpl/vector.hpp>#include <boost/mpl/string.hpp>#include <boost/mpl/map.hpp>#include <boost/mpl/at.hpp>#include <boost/utility/result_of.hpp>#include <boost/type_traits.hpp>#include <dlfcn.h>      //UNIX动态链接头文件#include "libtest.h"namespace mpl = boost::mpl;struct dl_front{    typedef mpl::string<'./','lib','test','.so'> so_name;    typedef int (*Func1)(int);    typedef int (*Func2)(int,int);    typedef mpl::string<'so_','func','1'> func1_name;    typedef mpl::string<'so_','func','2'> func2_name;    typedef mpl::map<        mpl::pair<func1_name,Func1>,        mpl::pair<func2_name,Func2>        > map_fun;};template<typename Front>class dl_back{private:    void* m_h = NULL;public:    dl_back()    {        m_h = dlopen(mpl::c_str<typename Front::so_name>::value,RTLD_NOW);    }    ~dl_back()    {        dlclose(m_h);    }    template<typename FuncName>    typename mpl::at<typename Front::map_fun,FuncName>::type    //返回类型    func_ptr()    {        typedef typename mpl::at<typename Front::map_fun,FuncName>::type result_type;        result_type pf = reinterpret_cast<result_type>(dlsym(m_h,mpl::c_str<FuncName>::value));        return pf;    }    template<typename FuncName,typename ... Args>   //可变参数模板    typename boost::result_of<typename mpl::at<typename Front::map_fun,FuncName>::type(Args...)>::type  //使用result_of推导函数的返回值    func(Args const& ... args)    {        typedef typename mpl::at<typename Front::map_fun,FuncName>::type func_type; //得到函数指针类型        func_type pf = reinterpret_cast<func_type>(dlsym(m_h,mpl::c_str<FuncName>::value)); //获取函数指针        return pf(args...); //直接调用函数指针    }    template<typename FuncName,typename ... Args>   //可变参数模板    typename boost::function_traits<typename boost::remove_pointer<typename mpl::at<typename Front::map_fun,FuncName>::type>::type>::result_type                //获取函数的返回值 //改用function_traits推导类型    func_V(Args const& ... args)    {        return func_ptr<FuncName>()(args...);    }};int main(){    dl_back<dl_front> dl;    std::cout << "----------------mp version 2-----------" << std::endl;    std::cout << dl.func<dl_front::func1_name>(10) << std::endl;    std::cout << dl.func<dl_front::func2_name>(10,20) << std::endl;    std::cout << "----------------mp version 2-----------" << std::endl;    std::cout << dl.func_V<dl_front::func1_name>(10) << std::endl;    std::cout << dl.func_V<dl_front::func2_name>(10,20) << std::endl;    return 0;}//g++-4.8 mp_version2.cpp -ldl -o mp_version2
原创粉丝点击