我的C++实践(6):模板与继承相结合的威力

    模板表示类的集合,让模板继承一个类与面向对象编程中的继承并没有本质的差别。但是在模板中使用继承有一些特别的地方,比如基类可以依赖于模板参数(例如继承B<T>,这称为依赖型基类)、甚至模板参数直接可以作为基类,这些所谓的参数化继承,再结合多重继承等C++中特有的继承机制,我们可以发挥模板和继承各自的优势,产生出很多的有趣的技术。
    1、命名模板参数。 C++中当多个模板参数有缺省实参时,若需要显式指定其中的某个实参,则其前面的所有缺省实参都要给出。比如BreadSlicer<T1,T2,T3,T4>,T1-T4的缺省实参分别为DefaultArg1-DefaultArg4,若T3要显式给定实参Custom,则必须这样写BreadSlicer<DefaultArg1,DefaultArg2,Custom>,即T1,T2的缺省实参也必须要显式地写出来,这非常麻烦。如果我们能这样写BreakSlicer<T3=Custom>,让其他的参数自动使用缺省实参,那就简洁多了。这时T1-T4就不仅仅是参数的占位符了,它相当于给参数取了一个名字,有了具体的意义,直接给名字赋一个实参,其他参数就会自动采用默认值。实际上,在Python语言中就有这种机制。比如定义函数def parrot(state='a stiff',action='voom')后,就可以像parrot(action='VOOM')这样来调用函数。但是C++并不支持这种语法。通过使用模板和多重继承,我们可以模拟出这种机制来。我们为每个模板参数设计一个辅助的类模板,通过这个类模板来指定需要的实参即可,比如BreadSlicer<Arg3_is<Custom> >,显式指定第三个实参为类Custom,其他参数就自动使用缺省实参,并且无需再写出来了。这里模板Arg3_is相当于给第3个参数T3命了名,用这个名字来指定实参,其他参数就可以自动使用缺省的实参,而无需再显式写出,大大地方便了使用。注意现在我们传过去的实参是Arg3_is<Custom>,因此我们还需要剥离Arg3_is,以获得真正的实参Custom。
    命名模板实参的基本实现思想:把缺省类型DefaultArg1-DefaultArg4封装到一个基类DefaultArgs中,用typedef给它们命名好名A1-A4。用Argn_is<T>来为模板的第n个参数指定实参T(n=1~4),Argn_is<T>继承了DefaultArgs,并把传进来的实参T用typedef命名为相应的An,以覆盖父类的相应缺省实参。模板中参数T1-T4的缺省实参都变成统一的DefaultArgs。类型选择器ArgsSelector<T1,T2,T3,T4>用来剥离Argn_is以获得其真正的实参T,它只要多重继承T1-T4即可。例如当传过来的T3是Arg3_is<Custom>,由于ArgsSelector继承了Arg3_is<Custom>,它能看到Arg3_is<Custom>中的A3,通过A3就可引用Custom。最后,我们在模板中组合ArgsSelector就可以通过A1-A4来引用各个实参了。代码如下:

    从代码中可以看出,现在模板BreadSlicer中要使用实参类型时,要通过ArgsSelector的A1-A4来引用。比如使用BreadSlicer<Arg3_is<Custom> >时,有T1=Arg3_is<Custom>,ArgsSelector通过ArgsDiscriminator<T1,1>间接地继承了Arg3_is<Custom>,它就能看到这个基类中的A3,而A3就是显式指定的Custom了。其他三个参数A1,A2,A4没有通过显式指定来覆盖,则使用的仍然是基类DefaultArgs中的缺省类型。还有几个要注意的地方:
    (1)我们并不没有让ArgsSelector<T1,T2,T3,T4>直接继承T1-T4,而是通过ArgsDiscriminator<T,D>来间接继承T1-T4。因为如果直接继承T1-T4的话,当使用BreadSlicer<>时,T1-T4全部变成了缺省的DefaultTypeArgs,这样从4个完全相同的基类继承会导致编译错误。现在继承了ArgsDiscriminator<T1,1>-ArgsDiscriminator<T2,4>(它们直接继承了T1-T4),这4个类是不同的基类,因此没有问题。
    (2)T1-T4的缺省实参并没有指定为统一的DefaultArgs,而它的一个直接子类DefaultTypeArgs,这可以让我们根据需要来增加一些不同的行为。
    (3)因为ArgsSelector<T1,T2,T3,T4>通过ArgsDiscriminator<T,D>间接地继承了T1-T4,使用时T1-T4指定为Argn_is<T>或者使用默认的DefaultTypeArgs,而DefaultTypeArgs及所有Argn_is都继承自DefaultArgs,这有可能会使ArgsSelector多次继承DefaultArgs,因此DefaultTypeArgs及所有Argn_is都使用虚继承来避免在ArgSelector中产生多份DefaultArgs子对象。
    (4)虽然继承层次比较深,但对于那些包含虚基类的辅助命名模板,我们自始至终都没有对它们进行实例化,因此不存在性能或内存耗费的问题。
    2、模板参数为空基类时的优化。 我们知道在C++中只包含类型成员、非虚成员函数和静态数据成员的类称为空类,C++中空类大小不能为0,一般会插入一个char型,即大小为1字节。注意当类有非静态数据成员、虚函数或虚基类时,在运行期就要耗费内存(比如有虚函数表vtable)。编译器一般实现了空基类优化技术(EBCO),即当空类作为基类时,子类对象中的基类子对象会优化掉,大小变为0(而不是1个字节)。但我们要注意,当类间接地多次继承同一个基类,导致对象中有同一基类的多个子对象时(即它们分配在同一地址空间上),就不会被优化掉了。
    在模板中,模板实参有可能是空类,这会导致模板中该实参类型的成员变量白白地多占据了1个字节的空间。若已知模板实参T必是类类型,且模板中有T类型的成员,也有其他的非空类型A的成员,我们就可以对两个成员进行压缩存储,设计一个压缩存储模板CompressedPair<T,A>并继承T,那个A类型的成员直接为模板的一个成员,而把那个T型的成员存储在压缩模板的基类子对象中,这样当T为空基类时就会被优化掉。代码如下:

    我们的模板MyClass有两个成员info和storage,info是T型变量(T有可能为空类),storage是一个具体的指针类型的变量,我们可以把这两个变量压缩存储在一个CompressedPair<T,int*>变量中,通过first()或second()函数来获得需要的T型成员和int*型成员。这里T是CompressedPair<T,int*>的基类,当T是空类时,存放了变量info基类子对象部分就会被优化掉,大小变成0。如果不使用压缩存储,则MyClass中有成员T info,即使T是空类,info也要占1个字节。当然这样会使MyClass的实现变得冗长,但是使用了压缩存储后,对使用者而言,许多模板库的性能都得到显著的提高,有时这是值得的。
    当然,这样的实现有一个限制,即T必须为类类型,当T可以是非类类型时(比如int),CompressedPair继承T就不行了。另外,当MyClass有两个模板参数T1,T2,有T1 a和T2 b成员变量时,我们也可以对这样的两个变量进行压缩存储。只要修改CompressedPair,让它同时继承T1和T2,并把两个变量都放在基类子对象中,以进行空基类的优化即可。Boost库有一个通用的压缩存储实现boost::compressed_pair。
    3、奇异递归模板模式(CRTP): 即派生类将本身作为模板参数传递的给基类。CRTP的一个简单应用是可以记录某个类的对象构造总个数。当然,对对象构造个数进行计数也可以在每个类自己的代码体里面写,只需引入一个整型的静态数据成员,分别在构造函数和析构函数中进行递增和递减操作。有了CRTP,我们就可以把这些功能抽离出来,设计一个用来记录某类型对象构造总个数的通用模板,让需要计数的类继承这个模板,并将本身作为模板参数传给它。代码如下:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值