C++ template —— 模板与继承(八)

16.1 命名模板参数
许多模板技术往往让类模板拖着一长串类型参数;不过许多参数都设有合理的缺省值,如:

template <typename policy1 = DefaultPolicy1,
                typename policy2 = DefaultPolicy2,
                typename policy3 = DefaultPolicy3,
                typename policy4 = DefaultPolicy4>
class BreadSlicer
{
    .......
};

一般情况下使用缺省模板实参BreadSlicer<>就足够了。不过,如果必须指定某个非缺省的实参,还必须明白地指定在它之前的所有实参(即使这些实参正好是缺省类型,也不能偷懒)。 跟这样的BreadSlicer<DefaultPolicy1, DefaultPolicy2, Custom>相比,BreadSlicer<Policy3 = Custom>显然更有吸引力。这也是本节要介绍的内容。

我们的考虑主要是设法将缺省类型值放到一个基类中,再根据需要通过派生覆盖掉某些类型值。这样,我们就不再直接指定类型实参了,而是通过辅助类完成,如BreadSlicer<Policy3_is<Custom> >。既然用辅助类做模板参数,每个辅助类都可以描述上述4个policy中的任意一个,故所有模板参数的缺省值均相同:

template <typename PolicySetter1 = DefaultPolicyArgs,
                typename PolicySetter2 = DefaultPolicyArgs,
                typename PolicySetter3 = DefaultPolicyArgs,
                typename PolicySetter4 = DefaultPolicyArgs>
class BreadSlicer
{
    typedef PolicySelector<PolicySetter1, PolicySetter2,
                                     PolicySetter3, PolicySetter4>
                Policies;
    // 使用Policies::P1, Policies::P2, ……来引用各个Policies
};


剩下的麻烦事就是实现模板PolicySelector。这个模板的任务是利用typedef将各个模板实参合并到一个单一的类型(即Discriminator),该类型能够根据指定的非缺省类型(如policy1-is的Policy),改写缺省定义的typedef成员(如Default Policies的DefaultPolicy1)。其中合并的事情可以让继承来干:

// PolicySelector<A, B, C, D>生成A, B, C, D作为基类
// Discriminator<>使Policy Selector可以多次继承自相同的基类
// PolicySelector不能直接从Setter继承
template <typename Base, int D>
class Discriminator : public Base{
};

template <typename Setter1, typename Setter2,
                typename Setter3, typename Setter4>
class PolicySelector : public Discriminator<Setter1, 1>,
                                public Discriminator<Setter2, 2>,
                                public Discriminator<Setter3, 3>,
                                public Discriminator<Setter4, 4>{
};

注意,由于中间模板Discriminator的引入,我们就可以一致处理各个Setter类型(不能直接从多个相同类型的基类继承,但可以借助中间类间接继承)。
如前所述,我们还需要把缺省值集中到一个基类中:

// 分别命名缺省policies为P1, P2, P3, P4
class DefaultPolicies
{
    public:
        typedef DefaultPolicy1 P1;
        typedef DefaultPolicy2 P2;
        typedef DefaultPolicy3 P3;
        typedef DefaultPolicy4 P4;
};

不过由于会多次从这个基类继承,我们必须小心以避免二义性,故用虚拟继承:

// 一个为了使用缺省policy值的类
// 如果我们多次派生自DefaultPolicies,下面的虚拟继承就避免了二义性
class DefaultPolicyArgs : virtual public DefaultPolicies{
};

最后,我们只需要写几个模板覆盖掉缺省的policy参数:

template <typename Policy>
class Policy1_is : virtual public DefaultPolicies
{
    public:
        typedef Policy P1;           //改写缺省的typedef
};

template <typename Policy>
class Policy2_is : virtual public DefaultPolicies
{
    public:
        typedef Policy P2;           //改写缺省的typedef
};

template <typename Policy>
class Policy3_is : virtual public DefaultPolicies
{
    public:
        typedef Policy P3;           //改写缺省的typedef
};

template <typename Policy>
class Policy4_is : virtual public DefaultPolicies
{
    public:
        typedef Policy P4;           //改写缺省的typedef
};

最后,我们把模板BreadSlicer实例化为:

BreadSlicer<Policy3_is<CustomPolicy> > bc;

这时模板BreadSlicer中的类型Polices被定义为:

PolicySelector<Policy3_is<CustomPolicy>,
                    DefaultPolicyArgs,
                    DefaultPolicyArgs,
                    DefaultPolicyArgs>

由类模板Discriminator的帮助,我们得到了图16.1所示的类层次。从中可以看出,所有的模板实参都是基类,而它们有共同的虚基类DefaultPolicies,正是这个共同的虚基类定义了P1, P2, P3和P4的缺省类型;不过,其中一个派生类Policy3_is<>重定义了P3。根据优势规则,重定义的类型隐藏了基类中的定义,这里没有二义性问题。

在模板BreadSlicer中,我们可以使用诸如Policies::P3等限定名称来引用这4个policy,例如:

template <... >
class BreadSlicer
{
    ...
    public:
        void print(){
        Policies::P3::doPrint();
    }
    ...
};

16.2 空基类优化

C++类常常为“空”,这就意味着在运行期其内部表示不耗费任何内存。这常见于只包含类型成员、非虚成员函数和静态数据成员的类,而非静态数据成员、虚函数和虚基类则的确在运行期耗费内存。

即使是空类,其大小也不会是0。在某些对于对齐要求更严格系统上也会有差异。

16.2.1 布局原则

C++的设计者们不允许类的大小为0,其原因很多。比如由它们构成的数组,其大小必然也是0,这会导致指针运算中普遍使用的性质失效。

虽然不能存在“0大小”的类,但C++标准规定,当空类作为基类时,只要不会与同一类型的另一个对象或子对象分配在同一地址,就不需要为其分配任何空间。我们通过实例来看看这个所谓的空基类优化(empty base class optimization, EBCO)技术:

// inherit/ebco1.cpp
#include <iostream>

class Empty
{
    typedef int Int;       // typedef 成员并不会使类成为非空
};

class EmptyToo : public EmptyToo
{
};

class EmptyThree : public EmptyToo
{
};

int main()
{
    std::cout << "sizeof(Empty) :             " << sizeof(Empty) << '
';
    std::cout << "sizeof(EmptyToo) :             " << sizeof(EmptyToo) << '
';
    std::cout << "sizeof(EmptyThree) :             " << sizeof(EmptyThree) << '
';
}

如果编译器支持空基类优化,上述程序所有的输出结果相同,但均不为0(见图16.2)。也就是说,在类EmptyToo中的类Empty没有分配空间。注意,带有优化空基类的空类(没有其他基类),其大小亦为0;这也是类EmptyThree能够和类Empty具有相同大小的原因所在。然而,在不支持EBCO的编译器上,结果就大相径庭(见图16.3)。

想想在空基类优化下,下例的结果如何?

// inherit/ebco2.cpp
#include <iostream>
class Empty
{
    typedef int Int;         // typedef 成员并没有使一个类变成非空
};

class EmptyToo : public Empty
{
};

class NonEmpty : public Empty, public EmptyToo
{
};

int main()
{
    std::cout << "sizeof(Empty) :            " << sizeof(Empty) << '
';
    std::cout << "sizeof(EmptyToo) :            " << sizeof(EmptyToo) << '
';
    std::cout << "sizeof(NonEmpty) :            " << sizeof(NonEmpty) << '
';
}

也许你会大吃一惊,类NonEmpty并非真正的“空”类,但的的确确它和它的基类都没有任何成员。不过,NonEmpty的基类Empty和EmptyToo不能分配到同一地址空间,否则EmptyToo的基类Empty会和NonEmpty的基类Empty撞在同一地址空间上。换句话说,两个相同类型的子对象偏移量相同,这是C++对象布局规则不允许的。有人可能会认为可以把两个Empty子对象分别放在偏移0和1字节处,但整个对象的大小也不能仅为1.因为在一个包含两个NonEmpty的数组中,第一个元素和第二个元素的Empty子对象也不能撞在同一地址空间(见图16.4)。

对空基类优化进行限制的根本原因在于,我们需要能比较两个指针是否指向同一对象,由于指针几乎总是用地址作内部表示,所以我们必须保证两个不同的地址(即两个不同的指针值)对应两个不同的对象。
虽然这种约束看起来并不非常重要,但是在实际应用中的许多类都是继承自一组定义公共typedefs的基类,当这些类作为子对象出现在同一对象中时,问题就凸现出来了,此时优化应该被禁止。

16.2.2 成员作基类
书中介绍了将成员作基类的技术。但对于数据成员,则不存在类似空基类优化的技术,否则遇到指向成员的指针时就会出问题。
将成员变量实现为(私有)基类的形式,在模板中考虑这个问题特别有意义,因为模板参数常常可能就是空类(虽然我们不可以依赖这个规则)。

16.3 奇特的递归模板模式
奇特的递归模板模式(Curiously Recurring Template Pattern, CRTP)这个奇特的名字代表了类实现技术中一种通用的模式,即派生类将本身作为模板参数传递给基类。最简单的情形如下:

template <typename Derived>
class CuriousBase
{
    ....
};

class Curious : public CuriousBase<Curious>         // 普通派生类
{
    ....
};

在第一个实例中,CRTP有一个非依赖型基类:类Curious不是模板,因此免于与依赖型基类的名字可见性等问题纠缠。不过,这并非CRTP的本质特征,请看:

template <typename Derived>
class CuriousBase
{
    ....
};

template <typename T>
class CuriousTemplate : public CuriousBase<CuriousTemplate<T> >       // 派生类也是模板
{
    ...
};

从这个示例出发,不难再举出使用模板的模板参数的方式:

template <template<typename> class Derived>
class MoreCuriousBase
{
    ....
};

template <typename T>
class MoreCurious : public MoreCuriousBase<MoreCurious>
{
    ....
};

CRTP的一个简单应用是记录某个类的对象构造的总个数。数对象个数很简单,只需要引入一个整数类型的静态数据成员,分别在构造函数和析构函数中进行递增和递减操作。不过,要在每个类里都这么写就很繁琐了。有了CRTP,我们可以先写一个模板:

// inherit/objectcounter.hpp

#include <stddef.h>

template <typename CountedType>
class ObjectCounter
{
    private:
        static size_t cout;    // 存在对象的个数
    
    protected:
        //缺省构造函数
        ObjectCounter(){
            ++ObjectCounter<countedType>::count;
        }
        // 拷贝构造函数
        ObjectCounter(ObjectCounter<countedType> const&){
            ++ObjectCounter<countedType>::count;
        }
        // 析构函数
        ~ObjectCounter(){
            --ObjectCounter<countedType>::count;
        }
    
    public:
        // 返回存在对象的个数:
        static size_t live(){
            return ObjectCounter<countedType>::count;
        }
};

// 用0来初始化count
template <typename CountedType>
size_t ObjectCounter<CountedType>::count = 0;

如果想要数某个类的对象存在的个数,只需让该类从模板ObjectCounter派生即可。以一个字符串类为例:

//inherit/testcounter.cpp

#include "objectcounter.hpp"
#include <iostream>

template <typename CharT>
class MyString : public ObjectCounter<MyString<CharT> >
{
    ....
};

int main()
{
    MyString<char> s1, s2;
    MyString<wchar_t> ws;

    std::cout << "number of MyString<char> : " << MyString<char>::live() << std::endl;
    std::cout << "number of MyString<wchar_t> : " << MyString<wchar_t>::live() << std::endl;
}

一般地,CRTP适用于仅能用作成员函数的接口(如构造函数、析构函数和小标运算operator[]等)的实现提取出来。

16.4 参数化虚拟性

C++允许通过模板直接参数化3种实体:类型、常数(nontype)和模板。同时,模板还能间接参数化其他属性,比如成员函数的虚拟性。

// inherit/virtual.cpp

#include <iostream>
class NotVirtual
{
};

class Virtual
{
    public:
        virtual void foo(){
    }
};

template <typename VBase>
class Base : private VBase
{
    public:
        // foo()的虚拟性依赖于它在基类VBase(如果存在基类的话)中声明
        void foo(){
            std::cout << "Base::foo() " << '
';
        }
};

template <typename V>
class Derived : public Base<V>
{
    public:
        void foo(){
        std::cout << "Derived::foo() " << '
';
    }
};

int main()
{
    Base<NotVirtual>* p1 = new Derived<NotVirtual>;
    p1->foo();      // 调用Base::foo()

    Base<Virtual>* p2 = new Derived<Virtual>;
    p2->foo();       // 调用Derived::foo()
}
原文地址:https://www.cnblogs.com/yyxt/p/5200345.html