Effective C++(条款41-44)

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

class和template都支持接口和多态。

1、对于class而言,接口是显式的(explicit),以函数签名(由函数名称、参数类型、返回类型构成)为中心。多态则是通过virtual函数发生于运行期

举个例子:

class Widget{
public:
      Widget();
      virtual ~Widget();
      virtual std::size_t size() const;
      virtual void normalize();
      void swap(Widget& other);
      ...
};
void doprocessing(Widget& w)
{
      if(w.size()>10&&w!=someNastWidget)
      {
            Widget temp(w);
            temp.normalize();
            temp.swap();
       }
}

w被声明为Widget,所以w必须支持Widget接口,我们可以在源码(比如.h文件中)找到这个接口,所以称为一个 显式接口,也就是它在源码中明确可见。Widget的某些成员函数是virtual,w对那些函数的调用将表现出 运行期多态,也就是将于运行期根据w的动态类型决定究竟调用哪一个函数



2、template和泛型变成的世界中,显式接口和运行期多态仍然存在,但重要性降低。反而是隐式接口和编译期多态比较重要。

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


编译期多态和运行期多态之间的差异:类似于“哪一个重载函数该被调用(发生在编译期)”和“哪一个virtual函数该被绑定(发生于运行期)”之间的差异。

显示接口和隐式接口的差异:通常显式接口是由函数的签名式构成,例如Widget class

class Widget{
public:
      Widget();
      virtual ~Widget();
      virtual std::size_t size() const;
      virtual void normalize();
      void swap(Widget& other);
      ...
};

其public接口是由一个构造函数、一个析构函数、三个成员函数和其参数类型、返回类型、常量性构成,当然也包括编译器产生的复制构造函数和赋值运算符。


隐式接口就完全不同了,它是由有效表达式组成。

template<typename T>
void doprocessing(T& w)
{
      if(w.size()>10&&w!=someNastWidget)
      {
            T temp(w);
            temp.normalize();
            temp.swap();
       }
}

凡是涉及到w的任何函数调用,例如operator》和operator!=,都有可能造成 template具现化,是这些调用得以成功,这样的具现行为发生在编译器。“以不同的template参数具现化”会导致调用不同的参数,这便是所谓的编译期多态。

在doProcessing函数开始的if条件中,由于操作符重载带来的可能性,T唯一需要做的就是返回以个类型为X的对象,而X对象加上一个int必须能够调用一个operator>,这个operator>不需要的非得取得一个类型为X的参数不可,也可以取得类型为Y的参数,只要存在 一个隐式转换将类型X的对象转换为类型为Y的对象。同样的道理,T并不需要支持operator!=,operator!=分别接收参数为X和Y类型的对象,T可悲转换为X类型,为someNastWidget类型可被转换为Y类型,这样就能有效调用operator!=。



加诸于template参数身上的隐式接口,就像加诸于class对象身上的显式接口一样真实,而且两者都是在编译期完成检查。就像你无法以一种“与class提供的显式接口矛盾”的方式使用对象,你也无法在template中使用“不支持template所要求的隐式接口”的对象一样,他们的代码都是不能通过编译的。





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

1、声明template参数时,前缀关键字class和typename可互换

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


2、请使用关键字typename标识嵌套从属类型名称:但不得在base class list(基类列)或member initialization list(成员初始列)内以它作为base class修饰符


template内出现的名称如果相依于某个template参数,称之为从属名称。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称。

一般性规则如下:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename,因为在缺省的条件下,嵌套从属类型名称不属于类型,在它之前加上关键字typename就是告诉编译器该嵌套从属类型是一个类型。

template<typename T>
void print2nd(const T& container)
{
      if(container.size()>=2){
            typename T::const_iterator iter(container.begin());
            ++iter;
            int value=*iter;
            std::cout<<value;
      }
}


“typename必须作为嵌套从属类型名称的前缀词”的例外就是:不得在base class list(基类列)或member initialization list(成员初始列)内以它作为base class修饰符

template<typename T>
class Derived:public Base<T>::Nested{//base class list中不允许typename
public: 
    explicit Derived(int x):Base<T>::Nested(x)//mem.init.list中不允许typename
    {
        typename Base<T>::Nested temp;//嵌套从属类型名称中,作为base class修饰符需加上typename
        ...
    }
    ...
};





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

从某种意义而言,当我们从面相对象的c++跨进模板c++,继承就不像以前那样畅行无阻了。看下面这个例子:

class MsgInfo{...}  //用来保存信息的类
template<typename Company>
class MsgSender{
public:
    ...  //构造函数、析构函数等
    void sendClear(const MsgInfo& info)
    {
        std::string msg;
        在这儿,根据info产生信息;
        Company c;
        c.sendCleartext(msg);
    }
    void sendSecret(const MsgInfo& info){...}//类似sendClear,唯一的不同是调用c.sendEncrypted
};

template<typename Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
    ...  //构造函数、析构函数等
    void sendClearMsg(const MsgInfo& info)
    {
        将“传送前”的信息写到log;
        sendClear(info);  //调用base class函数:这段代码无法通过编译
        将“传送后”的信息写到log;
    }
    ...
};


问题:C++不进入模板化基类内进行观察


原因:当编译器遭遇class template LoggingMsgSender定义时,并不知道它继承什么样的类。它继承MsgSender<Company>,其中的Company是一个模板参数,不到LoggingMsgSender被具体化无法确切知道它是什么,而不知道Company是什么,就无法知道class MsgSendser<Company>看起来像什么——更确切的说是没有办法知道它是否有一个sendClear函数。

C++知道base class template有可能被全特化,而那个特化版本可能不提供和一般性template相同的接口,因此它往往拒绝在templatized base class(模板化基类)(本例是MsgSender<Company>)内寻找继承而来的名称(本例是sendClear)。


解决办法: 1、在base class函数调用动作之前加上this->

                     2、使用using 声明

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


本例解决如下所示:

template<typename Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
    ...
    void sendClearMsg(const MsgInfo& info)
    {
        ...
        this->sendClear(info);//OK,假设sendClear将被继承
        ...
    }
    ...
};


template<typename Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
    using MsgSender<Company>::sendClear;//告诉编译器请它假设sendClear位于base class内
    ...
    void sendClearMsg(const MsgInfo& info)
    {
        ...
        sendClear(info);//OK,假设sendClear将被继承
        ...
    }
    ...
};


template<typename Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
    ...
    void sendClearMsg(const MsgInfo& info)
    {
        ...
        MsgSender<Company>::sendClear(info);//OK,假设sendClear将被继承
        ...
    }
    ...
};//这往往是最不让人满意的一个解法,如果被调用的是virtual函数,这样的明确资格修饰会关闭virtual绑定行为

上述解法做的事情就是:对编译器需哦出承诺,“base class template的任何特化版本都将支持其一般版本所提供的接口”。这种承诺是编译器解析像LoggingMsgSender这样的derived class template时需要的。



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

template生成多个类和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系

在non-template代码中,重复是十分明确的,你可以看到两个函数或类之间有所重复;但是在template代码中,重复是隐晦的,毕竟只有一份template源码,你必须训练自己去感受当template被具现化多次时可能发生的重复。


下面是template引出代码膨胀的一个典型例子:

template<typename T,std::size_t n>
class SquareMatrix{
public:
    ...
    void invert();//求逆矩阵
};
//调用
SquareMatrix<double,5> sm1;
...
sm1.invert();
SquareMatrix<double,10> sm2;
...
sm2.invert();//具现化两个invert,这两个函数除了常量5和10,其他部分都一样


1、因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可以消除,做法是以函数参数或class成员变量替换template参数

比如:

template<typename T>
class SquareMatrixBase{
protected:
    ...
    void invert(syd::size_t matrixSize);
    ...
};
template<typename T,std::size_t n>
class SquareMatrix:private SquareMatrixBase<T>{
private:
    using SquareMatrixBase<T>::invert;
public:
    ...
    void invert(){this->invert(n);}
};


还有其他的办法,这里就不做举例说明了。



2、因参数类型(type parameter)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现 类型共享实现码

例如,许多平台上int和long有完全相同的二进制表述,所以像vector<int>和vector<long>的成员函数有可能完全相同,这正是代码膨胀的最佳定义。某些连接器会合并完全相同的函数实现码,某些则不会,后者意味着某些template被具现化为int和long两个版本。

类似情况下,在大多数平台上,所有指针类型都有相同的二进制表述,因此凡template持有指针者(例如list<int*>、list<const int*>、list<SquareMatrix<long,3>*>等等),往往应该对每个成员函数使用唯一一份底层实现。这很具代表性的意味,如果你实现某些成员函数而他们操作强行指针(即T*),你应该令他们调用另一个操作无类型指针(即void*)的函数,由后者完成实际工作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值