zoukankan      html  css  js  c++  java
  • 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()
    }
  • 相关阅读:
    项目冲刺-第一天
    第四次个人作业-关于微软必应词典客户端 的案例分析
    第三次作业-结对编程
    学院教师开课管理网站项目
    使用Git进行代码管理的心得--github for windows
    十天冲刺---Day8
    十天冲刺---Day7
    十天冲刺---Day6
    十天冲刺---Day5
    十天冲刺---Day4
  • 原文地址:https://www.cnblogs.com/yyxt/p/5200345.html
Copyright © 2011-2022 走看看