模板与泛型编程

模板与泛型编程

Rule 41 了解隐式接口和编译期多态

面向对象编程世界总是以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。

Templates及泛型编程的世界,与面向对象有根本上的不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口(implicit interfaces )和编译期多态(compile-time polymorphism)移到前头了。

template<typename T>
void doProcessing(T& w)
{
    if (w.size()>10&&w!=someNastyWidget)
          T  temp (w);
          temp.normalize();
          temp.swap(w);
}
  • w必须支持哪一种接口,系由template中执行于w身上的操作来决定。本例看来 w的类型T好像必须支持。ize, normalize和swap成员函数、copy构造函数(用以建立temp)、不等比较(inequality comparison,用来比较someNasty-Widget).我们很快会看到这并非完全正确,但对目前而言足够真实。重要的是,这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口(implicit interface)。

  • 凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化(instantiated ),使这些调用得以成功。这样的具现行为发生在编译期。“以不同的template参数具现化function templates”会导致调用不同的函数,这便是所谓的编译期多态(compile-time polymorphism )。

在具象化的过程中,必须支持所有的隐式接口,否则编译失败。

  • classes和templates都支持接口(interfaces)和多态(polymorphism )。

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

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

Rule 42 了解typename的双重意义

template<class T>class Widget;//使用”Class"
template<typename T> class Widget;//使用”typename"

以上两种用法意义完全相同。 然而C++并不总是把class和typename视为等价。有时候你一定得使用typename。为了解其时机,我们必须先谈谈你可以在template内指涉(refer to)的两种名称。

假设我们有个template function,接受一个STL兼容容器为参数,容器内持有的对象可被赋值为ints。

template<typename C>
void print2nd(const C& container)          //打印容器内的第二元素
{//注意这不是有效的C++代码
  if (container.size()>=2){
      C::const_iterator iter(container.begin());//取得第一元素的迭代器
      ++iter;//将iter移往第二元素
      int value = *iter;//将该元素复制到某个int.
      std::cout<<value;                        //打印那个int.
  }
}
  1. 特别强调两个local变量iter和value. iter的类型是C::const_iterator,实际是什么必须取决于template参数C. template内出现的名称如果相依于某个template参数,称之为从属名称(dependent names )。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称(nested dependent name )。C::const_iterator就是这样一个名称。实际上它还是个嵌套从属类型名称(nested dependent type name ),也就是个嵌套从属名称并且指涉某类型。

  2. print2nd内的另一个local变量value,其类型是int。int是一个并不倚赖任的template参数的名称。这样的名称是谓非从属名称(non-dependent names )。

  • 嵌套从属名称有可能导致解析(parsing)困难。

template<typename C>
void print2nd(const C& container)
{
    C::const_iterator* x;
};

以上的C::const_iterator可以解析为C域中的对象,或者static实例与x相乘。所以为了让解析器能够顺利解释出含义。矫正这个形势,我们必须告诉C十十说C::const iterator是个类型。只要紧临它之前放置关键字typename即可:

template<typename C>
void print2nd(const C& container)
{
    if (container.size()>=2){
        typename C::const_iterator iter(container.begin());
      //...
    }
//这是合法的
};

例外情形:typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。例如:

template<typename T>
class Derived: public Base<T>::Nested{//base class list中
public://不允许’'typename".
    explicit Derived(int x)
    :Base<T>::Nested(x)                       //mem而t. list中
    {//不允许”ypename".
        typename Base<T>::Nested temp;//嵌套从属类型名称,
                                //既不在base class list中也不在mem. init. list中,
    }//作为一个base class修饰符需加上typename.
};
template<typename  IterT>
void workWithIterator(IterT iter)
typename std::iterator_traits<IterT>::value_type temp(*iter);
//名字过长 建议在函数内使用别名
typedef typename std::iterator_traits<IterT>::value_type temp(*iter) value_type;
  • 声明template参数时,前缀关键字class和typename可互换。

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

Rule 43 学习处理模板化基类内的名称

在子类模板中,继承自基类模板的函数会不可调用,因为无法保证实例化基类版本中含有相应的函数。

class CompanyA{
public:
void sendCleartext(const std::string& msg)
void sendEncrypted(const std::string& msg)
};
class CompanyB{
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
};

class MsgInfo{
template<typename Company>
 class MsgSender{
public:
//针对其他公司设计的classes.
//这个class用来保存信息,以备将来产生信息
//构造函数、析构函数等等。
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;
    }
};

问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然它继承的是MsgSender<Company>,但其中的Company是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。而如果不知道Company是什么,就无法知道class MsgSender<Company>看起来像什么—更明确地说是没办法知道它是否有个sendClear函数。

也就是子类不会把Base的函数当成是可搜索的空间。

注意以下特例,把sendClear删除。

class CompanyZ{
public:
//这个C}BSS不提供
//sendCleartext函数
void sendEncrypted(const std::string& msg);
};

template<>
class MsgSender<CompanyZ>{
public:
//一个全特化的
//MsgSender;它和一般template相同,
//差别只在于它删掉了sendClear
void sendSecret(const MsgInfo& info)
{...}
};

现在,MsgSender针对CompanyZ进行了全特化,让我们再次考虑derived class LoggingMsgSender:

template<typename Company>
class LoggingMsgSender:
public:
public MsgSender<Company>
void sendClearMsg(const MsgInfo& info)
{
  //将“传送前”的信息写至log;
  sendClear(info);//如果Company==CompanyZ,这个函数不存在·
  //将“传送后”的信息写至log;
}

因为那个class并未提供sendClear函数!那就是为什么C++拒绝这个调用的原因:它知道base class templates有可能被特化,而那个特化版本可能不提供和一般性template相同的接口‘因此它往往拒绝在templatized base classes(模板化基类,本例的MsgSender<Company>)内寻找继承而来的名称(本例的SendClear)。就某种意义而言,当我们从Object Oriented C++跨进下emplate C++(见条款1),

解决方案:

  1. 第一是在base class函数调用动作之前加上"this->":

  2. 第二是使用using声明式。如果你己读过条款33,这个解法应该会令你感到熟悉。条款33描述using声明式如何将“被掩盖的base class名称”带入一个derived class作用域内。

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

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
  //method 2
  using MsgSender<COmpany>::sendClear;   //告诉编译器,请它假设
    void sendClearMsg(const MsgInfo& info)
      {
     // 将“传送前”的信息写至log;
      //method 1
      this->sendClear(info);//成立,假设sendClear
      //method 3
      MsgSender<Company>::sendClear(info);
      //将“传送后”的信息写至log;
      }
};

以上方法相当于声明,在实际编译中根据具体情况看是否会失败。

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

Rule 44 将与参数无关的代码抽离templates

举个例子,假设你想为固定尺寸的正方矩阵编写一个template。该矩阵的性质之一是支持逆矩阵运算(matrix inversion)。

template<typename T,std::size t n>
class SquareMatrix{
public:
//!!template支持n x n矩阵
//类型为T的objects;见以下
//关于size
void invert();
//求逆矩阵
};

SquareMatrix<double,5> sml;//调用SquareMatrix<double,5>::invert
sml.invert();
SquareMatrix<doub1e,10> sm2;//调用SquareMatrix<double,l0>::invert
sm2.invert();

这样就会具化出两份代码。为了进一步抽象,组织成基类,把invert函数封装进去。

template<typename T>
class SquareMatrixBase{
protected:
//与尺寸无关的base class.
//用于正方矩阵
void invert(std::size_t matrixSize);
//以给定的尺寸求逆矩阵
};

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
private:
    using SquareMatrixBase<T>::invert;    //避免遮掩base版的
                                                  //invert;见条款33
public:
void invert(){this->rovert(n);
//制造一个inline调用,调用
//base class版的invert。稍后
//说明为什么这儿出现this->
};

SquareMatrixBase也是个template,不同的是它只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于某给定之元素对象类型,所有矩阵共享同一个(也是唯一一个)SquarMatrixBase class。它们也将因此共享这唯一一个class内的invert.

另一个要考虑的问题是矩阵所处的位置。可以通过基类成员变量,使用指针的方式从子类传到基类。

这个条款只讨论由non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀。例如在许多平台上int和long有相同的二进制表述,所以像vector<int>和vector<long>的成员函数有可能完全相同—这正是膨胀的最佳定义。

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

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

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

Rule 45 运用成员函数模板接受所有兼容类型

真实指针做得很好的一件事是,支持隐式转换(implicit conversions )。Derived class指针可以隐式转换为base class指针。

但如果想在用户自定的智能指针中模拟上述转换,稍稍有点麻烦。我们希望以下代码通过编译:

template<typename T>
Class SmartPtr{
    public:
    explicit SrnartPtr(T* realPtr);
//智能指针通常
//以内置(原始)指针完成初始化
};
SimartPtr<Top> ptl=
    SmartPtr<Middle>(new Middle);//将SmartPtr<Middle>转换为 SmnartPtr<Top>
SmartPtr<Top> pt2=
    SmartPtr<Bottom>(new Bottom);//将SYnartPtr<BOttom>转换为 SmartPtr<Top>

SmartPtr<const Top> pct2 = pt1;
//将SmartPtr<Top>转换为
//SmartPtr<const Top>

Templates和泛型编程(Generic Programming)

为了实行类型转换一个办法就是通过构造函数,但是我们没有办法预期我们的子类。所以必须写一个构造模板。这样的模板(templates)是所谓member function templates(常简称为member templates ),其作用是为class生成函数:

template<typename T>
class SmartPtr{
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other);
//member template,
//为了生成copy构造函数
};

以上代码的意思是,对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>—因为SmartPtr<T>有个构造函数接受一个SmartPtr<U>参数。这一类构造函数根据对象u创建对象t(例如根据SmartPtr<U>创建一个SmartPtr<T>),而u和v的类型是同一个template的不同具现体,有时我们称之为泛化(generalized) copy构造函数。

member function templates(成员函数模板)的效用不限于构造函数,它们常扮演的另一个角色是支持赋值操作。例如TRl的shared——ptr(见条款13)支持所有“来自兼容之内置指针、trl : : shared_ptrs, auto_ptrs和trl : : weak_ptrs(见条款54)”的构造行为,以及所有来自上述各物(trl::weak_ptrs除外)的赋值操作。以下是shared_ptr部分实现。

template<class T>
class shared_ptr{
public:
  template<class Y>
    explicit shared_ptr(Y* p);
  template<class Y>
    shared_ptr(shared_ptr<Y> const& r);
  template<class Y>
    explicit shared_ptr(weak_ptr<Y> const& r)
  template<class Y>
    explicit shared_ptr(auto_ptr<Y>& r);
  
  template<class Y> //赋值,来自任何兼容的
  shared ptr& operator=(shared ptr<Y> const& r);//share电ptr,
template<class y> //或auto- ptr.
  shared_ptr& operator=(auto_ptr<Y>& r);

上述所有构造函数都是explicit,惟有“泛化copy构造函数”除外。那意味从某个shared_ptr类型隐式转换至另一个shared_ptr类型是被允许的,但从某个内置指针或从其他智能指针类型进行隐式转换则不被认可.

  • 请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。

  • 如果你声明member templates用于泛化copy构造”或‘泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。

Rule 46 需要类型转换时请为模板定义非成员函数

在Rlue 24中已经讨论了为什么只有non-member函数才可以“在所有的实参身上实施隐式类型转换”。那么该条例就是为了将其模板化。

按照之前的写法模板化就变成:

template<typenameT>
  class Rational{
  public:
      Rational(const T& numerator=0,
                    const T& denominator=1);
      const T numerator()const;
      const T denominator()const;
  };
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs,
                               const Rational<T>& rhs);
    
Rational<int> oneHalf(1, 2);//这个例子来自条款24}
//唯一不同是Rational改为template.
Rational<int> result = oneHalf*2;//错误!无法通过编译。

编译失败,编译器找不到我们所需要的函数。因为它推断不出来我们所需要的类型。以oneHalf去推断可以推断出int类型。但是第二个实参是2.期盼的编译器使用Rational<int>的non-explicit构造函数将2转换为Rational<int>,进而将T推导为int,但它们不那么做,因为在template实参推导过程中从不将隐式类型转换函数纳入考虑。

既然不能推断出函数的具体签名,也就不能产生该函数。

接下来,只能利用类模板,把函数声明在类里面,作为friend函数,在类具现化的时候把函数同时声明。

template<typename T>
class Rational{
public:
friend//声明
const Rational operator*(const Rational& lhs,//operator*函数,
                  const Rational& rhs);//细节详下。
};
template<typename T> //定义
const Rational<T> operator*(const Rational<T>&  lhs,   //operator*函数。
                                        const Rational<T>& rhs){}

以上代码通过编译,但是在连接的时候失败了。因为以上根本没有实现operator*函数。

template<typename T>
class Rational{
public:
friend const Rational operator*(const Rational& lhs,
                                const Rational& rhs){
return Rational(lhs.numerator()*rhs .numerator(),//实现码与
                          lhs .denominator()*rhs .denominator());//条款24同
    }
};

把实现放在类模板中就可以实现这个声明的同时完成定义。

接下来讨论friend函数。friend本意上访问class的not-public部分,这和本意无关。为了让类型转换,又必须是一个non-member函数为了在class内部声明一个non-member函数(在具现化类的时候产生声明及定义)的唯一办法就是让它成为friend函数。

  • 当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数。

Rule 47 请使用traits classes表现类型信息

traits classes是一类tag,需要类定义以及具体的traits配合实现。在这里不做阐述。

  • Traits classes使得“类型相关信息”在编译期可用。它们以templates和“templates特化”完成实现。

  • 整合重载技术(overloading)后,traits classes有可能在编译期对类型执行if...else测试。

Rule 48 认识template元编程

TMP有两个伟大的效力。第一,它让某些事情更容易。如果没有它,那些事情将是困难的,甚至不可能的。第二,由于template metaprograms执行于C++编译期,因此可将工作从运行期转移到编译期。这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译期找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期移转至编译期的另一个结果是,编译时间变长了。是的,程序如果使用TMP,其编译时间可能远长于不使用TMP的对应版本。

  • Template metaprogramming < TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。

  • TMP可被用来生成“基于政策选择组合”( based on combinations of policy choices )的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值