Effective C++读书笔记八—— 条款41-44

模板与泛型编程

条款41:了解隐式接口和编译期多态

本节两个概念即什么是隐式接口,什么是编译期多态。

面向对象编程世界总是以显示接口(expicit interfaces)和运行期多态(runtime polymorphism)解决问题。但是在templates 及泛型编程的世界,隐式接口和编译期多态更重要。

隐式接口是相对于函数签名(也就是函数名称、参数类型、返回类型)所代表的显式接口而言的。当我们看到一个函数签名,比如说:

string GetNameByStudentID(int StudentID);

我们就知道这个函数有一个整型的形参,返回值是string。

隐式接口是由有效表达式组成的,考虑一个模板函数,像下面这样:

1 template
2 void TemplateFunction(T& w)
3 {
4 if(w.size() > 10){…}
5 }
T可以是int,可以double,也可以是自定义的类型。光看这个函数声明,我们不能确定T具体是什么,但我们知道,要想通过编译,T必须要支持size()这个函数。也就是说,T中一定要有这样的函数接口声明。

ReturnValue size();

当然返回值ReturnValue不一定是int了,只要它能支持operator > (ReturnValue, 10)这样的运算即可。这种由表达式推判出来的函数接口,称之为隐式接口。

简言之,显式接口由函数签名式构成,隐式接口由有效的表达式组成。

至于编译期多态,我们在讨论继承时就已经多次提到“运行时多态”了,它伴随着virtual关键字,本质是一个虚表和虚指针,在类对象构造时,将虚指针指向了特定的虚表,然后运行时就会根据虚表的内容进行函数调用。

“编译期多态”,从字面上来看,它发生在编译阶段,实际上就是template 这个T的替换,它可以被特化为int,或者double,或者用户自定义类型,这一切在编译期就可以决定下来T到底是什么,编译器会自动生成相应的代码(把T换成具体的类型),这就是编译期多态做的事情了,它的本质可以理解成自动生成特化类型版本,T可以被替换成不同的类型,类似于函数重载,比如同时存在int版本的swap与double版本的swap,形成函数重载。
运行时多态是决定“哪一个virtual函数应该被绑定”,虚函数在子类继承中的多态性。

记住:

  1. class和templates都支持接口与多态;

  2. 对classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期;

  3. 对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。

条款 42:理解typename的双重意义

问题引入:以下template声明中,class和typename有什么不同?

template <class T> class Widget;
template <typename T> class Widget; 

答案:没有不同。当我们声明typename类型参数。class和typename的意义完全相同。

假设我们要打印一个容器(里面为)中的第二个元素,那么函数应该是这样:

template <typename C> 
void print2nd(const C& container)  
{  
    if(container.size() >= 2)  
    {  
        C::const_iterator iter(container.begin());  
        ++iter; 
        int value = *iter; 
        std::cout<<*iter;  
        C::const_iterator *x; //容易引起歧义
    }  
}

iter local变量,类型是C::const_iterator,实际是什么取决于template参数C。 template内出现的名称如果依赖于某个template参数,就称作从属名称。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称。
value类型是int 。int 是一个并不依赖于任何template 参数的名称,叫做非从属名称(non-dependent names)。

嵌套从属名称容易导致解析困难。比如上例中 C::const_iterator x ; 看起来x是一个local变量,但是也可能会被误认为C:: const_iterator 是个类型变量 是个相乘操作。所以需要告诉编译器 C::const_iterator 是个类型。在C::const_iterator 前面放置关键字 typename 即可。

总结:一般性规则,任何时候当你想在template中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置放上关键字 typename。

但是总有例外:typename不能出现在基类列表的嵌套从属类型之前,也不可出现在成员初始化列表中作为类型的限定符。这样的例外并不容易操作。

template T>
class Derived:public Based<T>::Nested{ //base class list 中不允许typename
public:
explicit Derived(int x)
:Based<T> ::Nested(x)     //member ini.list 不允许typename
{
typename Base <T>:Nested temp; //嵌套从属类型不在两个例外之内,所以要加typename
...
}
...
};

记住:
◆当声明template参数时,typename与class可以互换
◆使用typename标识嵌套从属类型名称,但是在基类列表和成员初始哈列表中,不能使用typename来指明从属名称。

条款 43:学习处理模板化基类内的名称

我们必须有某种办法令C++“不进入templatized base class观察”的行为失效。有一下三种办法

1、在base class函数调用动作之前加上“this->”

    template<typename Company>
    class LoggingMsgSender: public MsgSender
    {
    public:
        ……//析构构造等
        void SendClearMsg(const MsgInfo& info)
        {
            //发送前的信息写到log
            this->sendClear(info);
            //传送后信息写到log
        }
    };

2、使用using声明式,有点类似**条款**33。

    template<typename Company>
    class LoggingMsgSender: public MsgSender
    {
    public:
        ……//析构构造等
        uinsg MsgSender::sendClear;//告诉编译器,假设sendClear位于base class内
        void SendClearMsg(const MsgInfo& info)
        {
            //发送前的信息写到log
            sendClear(info);//可以编译通过,假设sendClear将被继承
            //传送后信息写到log
        }
    };

补充一下,这里情况和**条款**33不同,这里不是将被掩盖的base class名称带入一个derived class作用域内,而是编译器不进入base class作用域内查找,通过using告诉编译器,请它去查找。

3、明白指出被调用的函数位于base class内

    template<typename Company>
    class LoggingMsgSender: public MsgSender
    {
    public:
        ……//析构构造等
        void SendClearMsg(const MsgInfo& info)
        {
            //发送前的信息写到log
            MsgSender::sendClear(info);//可以编译通过,假设sendClear被继承
            //传送后信息写到log
        }
    };

这种做法使用了明确资格修饰符(explicit qualification),这将会关闭virtual绑定行为。

回头再看上面三种做法,原理相同:对编译器承诺“base class template的任何特化版本都将支持其一般(泛化)版本所提供的接口”。这样的承诺是编译器在解析(parse)像LoggingMsgSender这样的derived class template时需要的。但如果这个承诺稍后没有兑现,编译器还会给事实一个公道。例如,稍后源码内:

    LoggingMsgSender zMsgSender;
    MsgInfo msgData;
    zMsgSender.sendClearMsg(msgData);//错误,无法编译通过

在调用sendClearMsg这个点上,编译器直到base class是个template特化版本,且它直到这个特化版不提供sendClearMsg函数。

总结一下,本条款讨论的是,面对指涉base class member之无效references,编译器的诊断时间可能发生在早起(当解析derived class template定义时),也可能发生在晚期(当template被特定值template实参具体化时)。C++宁愿较早诊断,这也是为什么当base classes从template具体化时,它假设对那些base classes的内容毫无所悉的缘故。

记住:

可在derived class templates内通过this->指涉base class templates内的成员名称,或藉由一个明白写出base class资格修饰符完成。

条款44:将与参数无关的代码抽离templates

Templates可以节省时间和避免代码重复。对于类似的classes或functions,可以写一个class template或function template,让编译器来做剩余的事。这样做,有时候会导致代码膨胀(code bloat):其二进制码带着重复(或几乎重复)的代码、数据,或者两者。但这时候源代码看起来可能很整齐。

先来学习一个名词:共性与变性分析(commonality and variability analysis)。比较容易理解。例如,你在编写几个函数,会用到相同作用的代码;这时候你往往将相同代码搬到一个新函数中,给其他几个函数调用。同理,如果编写某个class,其中某些部分和另外几个class相同,这时候你不会重复编写这些相同部分,只需把共同部分搬到新class中去即可,去使用继承或复合(**条款**32,38,39),让原先的classes取用这些共同特性,原classes的互异部分(变异部分)仍然留在原位置不动。

编写templates时,也要做相同分析,避免重复。non-template代码中重复十分明确:你可以看到两个函数或classes之间有所重复。但是在template代码中,重复是隐晦的,因为只有一份template源码。

例如,你打算在为尺寸固定的正方矩阵编写一个template,该矩阵有个支持逆矩阵运算的函数

 template<typename T, std::size_t n>//T为数据类型,n为矩阵大小
    class SquareMatrix{
    public:
        ……
        void invert();//求逆运算
    };
    SquareMatrix<double,5> sm1;
    sm1.invert();//调用SquareMatrix::invert
    SquareMatrix<double,10> sm2;
    sm2.invert();//调用SquareMatrix::invert

上面会具体化两份invert。这两份函数几乎完全相同(除了一个操作5*5矩阵,一个操作10*10)。这就是代码膨胀的一个典型例子。

上面两个函数除了操作矩阵大小不同外,其他相同。这时可以为其建立一个带数值的函数,而不是重复代码。于是有了对SquareMatrix的第一份修改:

 template<typename T>
    class SquareMatrixBase{
    protected:
        void invert(std::size_t matrixSize);
        ……
    };
    template<typename T, std::size_t n>
    class SquareMatrix:private SquareMatrixBase{
    private:
         using SquareMatrixBase::invert();//编码遮掩base中的invert,**条款**33
    public:
    ……
        void invert()//求逆运算
            {
                this->invsert(n);//稍后解释为什么用this
            }
    };

SquareMatrixBase::invert只是企图避免derived classes代码重复,所以它以protected替换public。这个函数使用this->,因为模板化基类内的函数名称会被derived classes掩盖(条款**43)。注意,SquareMatrixBase和SquareMatrix之间继承关系是private,这说明base class是为了帮助derived classes实现,两者不是**is-a关系。

现在还有一个问题,SquareMatrixBase::invert操作的数据在哪?它在参数中直到矩阵大小,但是矩阵数据derived class才知道。derived class和base class如何联络?一个做法是可以为SquareMatrixBase::invert添加一个参数(例如一个指针)。这个行得通,但是考虑到其他因素(例如,SquareMatrixBase内还有其他函数,也要操作这些数据),可以把这个指针添加到SquareMatrixBase类中。

    template
    class SquareMatrixBase{
    protected:
        SquareMatirxBase(std::size_t n,T* pMem)
        :size(n), pData(pMem){}
        void setDataPtr(T* ptr) {pData=ptr;}
        ……
    private:
        std::size_t size;
        T* pData;
    };
    template
    class SquareMatrix:private SquareMatrixBase{
    public:
        SquareMatrix()
        :SquareMatrixBase(n, data){}
        ……
    private:
        T data[n*n];
    };

这种类型的对象不需要动态分配内存,但是对象自身可能非常大。另一个做法是把矩阵数据放到heap

templateT, std::size_t n>
    class SquareMatrix:private SquareMatrixBase<T>{
    public:
        SquareMatrix()
        :SquareMatrixBase(n, 0),
        pData(new T[n*n])
        {this->setDataPtr(pData.get());}
        ……
    private:
        boost::scoped_array pData;
    };

这样以来,类型相同的derived classes会共享base class。

记住:
◆ template 生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系
◆ 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数
◆ 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述(binary representation)的具现类型共享实现码。

你可能感兴趣的:(c++,编译器多态,typename,模板)