关于C++变长模板参数的一些应用讨论

目录

导语

C++的变长模板参数

变长模板参数概述

与函数变长参数的类比

模板在软件设计中优缺点

变长模板参数实现用组合

 继承与组合

变长模板实现组合

 存在的问题

用变长模板实现责任链模式

 什么是责任链模式

 变长模板实现责任链模式

优点

缺点

结语


导语

C++是由C语言发展而来,基本继承了C语言的绝大部分特性和优点。其在C语言的面向过程基础之上,逐步支持面向对象和泛型编程。随着C++标准的不断发展,这些不同的范式也在不断的完善。

作为一种支持多种范式的编程语言,对于同样的业务设计方案,C++有时可以通过不同的方式实现。这大大的增加了程序设计的选择空间和灵活性,但这也是C++语言复杂的原因之一。

其中,C++11标准引入了很多新的特性,模板的变长参数就是重要的一种。有了模板的变长参数,在此之前不易实现的数据结构或方案,现在实现更加容易,STL中的元组(tuple)就是典型的应用。

本文主要是讨论使用C++的变长模板参数实现一些设计模式,以期充分利用C++模板和设计模式的两方面优点。

C++的变长模板参数

变长模板参数概述

模板是C++中实现泛型编程的手段。在面向过程和面向对象编程中,我们操作的是变量。而在模板编程中,被操作的是类型,这些类型可以是基本类型,如int、char、float,也可以是自定义的类型class。不同在于,前者是在执行期完成,而后者是在编译期完成的。

模板的变长参数,允许任意个数、任意类型的模板参数,不必在模板定义的时候固定参数个数。其会在编译期根据实例化的参数类型进行展开。

以tuple为例

template< class... Types > class tuple;

    在使用tuple的时候,可以指定多个类型参数,甚至不指定任何参数

Std::tuple<> var1;

Std::tuple<int,char> var2;

Std::tuple<int,std::string> var3;

变长模板的实现一般依赖于模板的递归展开和偏特化完成。

以简化版的tuple为例。

template<typename... _Types>
class tuple;

 
template<typename T>
class tuple<T>              //1
{
public:
    tuple(){}
    int sum() const
    {
        return 0;
    }
    //......
protected:
    T value;
};

 
template<typename T, typename... Args>
class tuple<T,Args...> : public tuple<Args...>     //2
{
public:
    tuple(T arg,Args... args):tuple<Args...>(args...)
    {
        value =arg;
    }
    //......
protected:
    T value;
};

在实例化tuple模板的时候,如果只有一个类型参数,则直接特化1处的tuple。

如果有N个类型参数(N>1),会匹配到2处的tuple,此时2中的tuple会将N个参数分为参数T和N-1个的变长参数Args,并继承Args实例化的tuple,而其有N-1个类型参数。这样依次递归,直至只剩一个类型参数,根据偏特化匹配到1处的tuple。

这样最终通过递归展开和偏特化实现了任意个参数类型的模板展开。

与函数变长参数的类比

函数的变长参数,从C语言就已经开始支持。C++11之后的模板变长参数,在语法层面与前者有几分相似,但两者有根本的区别。异同点主要有以下几点:

相同点:

  1. 均不用在定义期指定参数个数或类型。

  2. 均需要在调用的地方(模板是在模板实例化的位置),明确的指定参数类型或个数。

不同点:

  1. 函数变长参数是在运行期进行展开,模板变长参数是在编译期进行展开。

  2. 函数变长参数需要调用者专门的参数,用于指定后续参数的个数或类型,例如printf中的后续的变长参数是通过format中的占位符指定。而在模板变长参数中,不用明确指定参数个数,编译器会根据实例化的参数列表进行解析确定。

    模板在软件设计中优缺点

优点:

  1. 编译期间完成运算展开,提高运行期的性能

  2. 突破类型限制壁垒,做到代码复用

  3. 抽象数据结构及算法,减少重复造轮子,使代码更简洁

  4. 编译期间语法检验,提高程序健壮性

缺点:

  1. 模板代码本身可读性较差

  2. gdb 时展开名称过长,debug不方便

  3. 声明与定义不易分离,不便实现定义封装

  4. 多次展开,bin文件过大

变长模板参数实现用组合

 继承与组合

在面向对象的编程范式中,继承和组合是两个很常见的设计模式。继承代表了“是什么”的概念,而组合代表了”有什么”的概念。

class Person

{

public:

    unsigned age;

    std::string name;

};



class Adult //成人

{

public:

    std::string mate_name;

}



class Child //小孩

{

public:

    std::string father_name;

};


class Teacher //老师

{

public:

    std::string cource;     //教授课程

    unsigned teach_years;   //教龄

};



class Student //学生

{

public:

    unsigned grade;     //年级

    std::string class;  //班级

    std::vector<unsigned> scores;   //分数

};

如上,有五个类,分别是Person、Adult、Child、Teacher、Student。

按照继承的方式,是Adult、Child、Teacher、Student直接继承自Person。按照组合的方式,是Adult、Child、Teacher、Student包含一个Person的成员。当然,此处的几个类的现实意义更适合用继承。

广义的说,“是什么”就“有什么”的一种特殊情况。我们在说,Teacher是Person的同时,其实是在说Teacher有了Person的所有属性。所以,从业务意义上来说,组合和继承是没有明显的界限的。

但在方案设计时,组合和继承还是有差别的。

 

组合

继承

语法访问方式

多一次引用(点操作符)

直接访问

绑定强度

弱绑定

强绑定

多态支持

不支持

支持

属性组合灵活性

较好

较差

变长模板实现组合

就以上来看,用组合和继承各有利弊,那么能不能有一种方式可以兼顾两者的部分优点呢。可以通过模板的变长参数来实现。


template<typename... Args>

class Combination;



template<typename T>

class Combination<T> : public T

{

};



template<typename T,typename... Args>

class Combination<T,Args...> : public Combination<Args...>,public T

{

};

typedef Combination<Person,Adult> Person1;

typedef Combination<Person,Adult,Teacher> Person2;

typedef Combination<Person,Adult,Teacher,Student> Person3;

typedef Combination<Person,Child,Student> Person4;

从以上的语法来看,此处的组合是通过多重继承来实现的。

具有继承的如下优点:

  1. 这样,在Person2、Person3、Person4这些类型中,可以直接通过点操作符访问Person、Adult、Child、Teacher、Student中的成员,不用多一次应用。

  2. 通过模板展开可以发现,组合之后的类型可以是其中任何一个的子类,可以支持动态多态。

 

除此之外,也支持了组合的灵活性。其中Person1、Person2、Person3、Person4就是通过不同的组合定义了不同身份的人。

 存在的问题

  1. 模板变长参数的组合实现使用了多重继承。

  2. 由于其本质上是继承实现的,所以在 属性类型中如果有同名变量,存在访问的歧义或冲突问题。

用变长模板实现责任链模式

 什么是责任链模式

责任链模式就是用于处理特定业务或数据的一条执行链,执行链上有多个节点或是模块,每个节点都有可能处理数据。在执行链的整个执行过程中,可以在任何一个节点之后就结束执行。

责任链模式是典型的一种顺序执行模式。在软件设计中,我们会经常有意无意的使用其中的思想。

主要有几种实现方式:

  1. 硬编码方式。依次调用并执行各个模块,每个模块执行完成后做执行状态判断以决定是否继续执行。

  2. 模块数组方式。将函数指针或是函数对象动态的放入数组,在需要执行相关业务的时候,循环的遍历数据执行其中的每一个模块,并判断执行状态。

  3. 模块链表方式。将多个模块以链表的方式串联,在上一模块的成员中记录下一模块的引用。这也是设计模式中常用的实现模式。

不管是哪种实现方式,本质上都是记录执行链中的模块顺序,执行时依次调用。区别在于模块的记录方式、执行方式的不同以及模块是否可动态改变。

但是三种方式都存在一定的问题。

硬编码方式:重复的代码调用,不简洁。

节点数组方式,节点链表方式:由于C++是强类型约束的,这要求各个模块(不管是函数还是类)必须是相同的类型或是同样的父类。这大大约束了使用场景。

 变长模板实现责任链模式

如果通过模板的变长模板参数来指定各个模块,而并在模板内部完成各个模块的调用,就可以简单的实现责任链的抽象封装。


template<typename T, typename FUNC_T>

int execute_chain(T& param, FUNC_T& func)

{

    return func(param);

}





template<typename T, typename FUNC_T, typename... Args>

int execute_chain(T& param, FUNC_T& func, Args&... executors)

{

    if(func(param) !=0)

        return -1;

    else

        return execute_chain(param, executors...);

    return 0;

}

FUNC_T可以是普通的函数也可以是支持括号运算符的类。execute_chain中的模板参数中,第一个参数是真正要处理的数据类型,后续的为依次要执行的模块类型。同理,函数参数中, param是实际的数据参数,executors是各个实际执行模块。

在execute_chain中,param和executors均是通过引用的方式传递的,这是为了防止对参数和模块类的拷贝构造。

具体使用方式如下:

typedef struct _Parames

{

}Parames;



class C1

{

public:

    C1()

    {

        std::cout<<"in C1"<<std::endl;

    }



    int operator()(Parames& parames)

    {

        std::cout<<"execute C1"<<std::endl;

        return 0;

    }

};



class C2

{

public:

    C2()

    {

        std::cout<<"in C2"<<std::endl;

    }



    int operator()(Parames& parames)

    {

        std::cout<<"execute C2"<<std::endl;

        return 0;

    }

};

int test_func(Parames& parames)

{

    std::cout<<"execute test_func"<<std::endl;

    return 0;

}



int main(int argn, char* argv[])

{

    C1 c1;

    C2 c2;

    auto func1 = test_func;

    Parames parames;



execute_chain(parames, c1,c2,func1,c2,c1,c2,func1);



return 0;

}

优点

  1. 打破了对类型的约束,可以执行不同类型的模块。

  2. 业务代码简洁、清晰。

缺点

  1. 数据参数个数唯一。为了区分模板参数中哪些是数据,哪些是模块,必须要固定数据参数的个数。

  2. 各个模块必须都支持括号运算符。

  3. 无法动态指定模块。

结语

作为C++的新特性,变长模板参数可以在很多地方发挥其强大的作用,其递归的展开方式也丰富了其使用场景。很多比较繁复的实现都可以通过它来实现。

例如boost中的bind函数为了支持不同个数的参数,定义了多个版本的实现。如果使用C++模板变长参数,就可以实现任意个数的bind函数,而且代码会更加的简洁。

本文所讨论的实现方式,只是众多方式中的一种,并不具有广泛的通用性。我们应该根据自身业务特性和设计方案,确定自己的诉求点,然后在多种实现方式中选择满足诉求的设计。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值