EC读书笔记系列之17:条款41、42、43、44、45、46

时间:2021-08-12 09:48:22

条款41 了解隐式接口与编译器多态

记住:

★classes和templates都支持接口和多态

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

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

条款42 了解typename的双重意义

记住:

★声明template参数时,前缀关键字class和typename可互换(函数模板或类模板均可!!!)

★请使用关键字typename标识嵌套从属类型名称;但不得在base class lists或成员初始列内以它作为base class修饰符

---------------------------------------------------------------------

template<class T> class Widget;

template<typename T> class Widget;

两者是等效的

----------------------------

但有时一定得使用typename:

template<typename C>
void print2nd( const C& container ) {
if( container.size() >= ) {
C::const_iterator iter( container.begin() ); //不能通过编译
++iter;
int value = *iter;
std::cout << value;
}
}

template内出现的名称若相依于某template参数,称之为从属名称。若从属名称在class内呈嵌套状,称之为嵌套从属名称,如上面的C::const_iterator;而上面的int则是非从属名称。

上面代码不能通过编译的原因是:

  iter声明式只有在C::const_iterator是个类型时才合理,但我们并未告诉C++说它是,欲矫正此,我们必须告诉C++说其是一个类型:

template<typename C>
void print2nd( const C& container ) {
if( container.size() >= ) {
typename C::const_iterator iter( container.begin() );
...
}
}

一般性规则:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在前放置关键字typename,但这一规则的例外是:typename不可出现在base class 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
Base<T>::Nested temp; //此处则必须加!!!
...
}
...
};

------------

最后一个例子:

template<typename IterT>
void workWithIterator( IterT iter ) {
typename std::iterator_traits<IterT>::value_type temp( *iter );
...
}

可用typedef简写为:

template<typename IterT>
void workWithIterator( IterT iter ) {
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp( *iter );
...
}

注:std::iterator_traits<IterT>::value_type是标准traits class的运用,相当于“类型为IterT之对象所指之物的类型”。如IterT是vector<int>::iterator,则temp的类型就是int。

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

记住:

★可在derived class templates(模版化派生类)内通过“this->”指涉base class templates内的成员名称,或借由一个明白写出的“base class资格修饰符”完成

----------------------------------------------------------------------------------------------

问题背景:

class CompanyA {
public:
...
void sendCleartext( const std::string &msg );
void sendEncrypted( const std::string &msg );
...
}; class MsgInfo { ... }; template<typename Company>
class MsgSender { //模板化基类
public:
...
void sendClear( const MsgInfo &info ) {
std::string msg;
… //在此根据info产生信息
Company c; //用到了template参数!
c.sendCleartext( msg );
} void sendSerect( 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函数,无法通过编译,会提示sendClear不存在
//将传送后的信息写至log;
}
};

原因分析:

当编译器遭遇类模板LoggingMsgSender定义式时,由于其继承的是MsgSender<Company>,但其中的Company是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。而若不知道Company是什么,就无法知道MsgSender<Company>是否有个sendClear函数。

解决方法:三个方法

方法一:在base class函数调用动作之前加上this->

template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
...
void sendClearMsg( const MsgInfo &info ) { //将传送前的信息写至log;
this->sendClear( info ); //成立,假设sendClear将被继承
//将传送后的信息写至log; }
};

方法二:使用using声明式

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将被继承下来
...
}
};

方法三:明白指出被调用的函数位于模板化基类内

缺点是∵若被调用的是虚函数,此法会关闭“virtual绑定行为”!!!

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

-------------------------------------

问题加深一点:为什么C++拒绝在模板化基类内寻找继承而来的名称呢?

接着上面提出的背景例子来说:

假设有个class CompanyZ坚持使用加密通讯:

class CompanyZ {
public:
...
//此class不提供sendCleartext函数
void sendEncrypted( const std::string &msg );
...
};

可见这时一般性的MsgSender template对CompanyZ并不合适,∵那个template提供了一个sendClear函数,而这对CompanyZ对象并不合理。欲矫正此问题,可以针对CompanyZ产生一个MsgSender特化版:

template<> //一个全特化的MsgSender,差别仅在于删掉了sendClear
class MsgSender<CompanyZ> {
public:
...
void sendSerect( const MsgInfo &info ) {
...
}
};

class定义式开头的template<> 语法象征这既不是template也不是标准class,而是个特化版的MsgSender template,在template实参是CompanyZ时被使用。这是所谓的模板全特化:template MsgSender针对类型CompanyZ特化了,而且其特化是全面性的,也就是说一旦类型参数被定义为CompanyZ,再没有其他template参数可供变化。

再次考虑派生类LoggingMsgSender:

template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
...
void sendClearMsg( const MsgInfo &info ) {
...
sendClear( info ); //若Company=CompanyZ,这个函数不存在
...
}
};

正如注释所言,当基类被指定为MsgSender<CompanyZ>时这段代码不合法,∵那个class并未提供sendClear函数!此即为啥c++拒绝这个调用的原因:他知道模板化基类有可能被特化,而那个特化版本可能不提供和一般性template相同的接口,∴其往往拒绝在模板化基类内寻找继承而来的名称。就这种意义来说,当从OOC++跨进Template C++,继承就不像以前那般畅行无阻了!!!

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

记住:

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

★因非类型模板参数造成的代码膨胀,往往可消除,做法是以函数参数或者class成员变量替换template参数(本条款主要讨论这条!!!

★因类型模板参数造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码(书中只最后一段提了一下)

--------------------------------------------------------------------------

举一个代码膨胀的例子(此处是因非类型模板参数造成):

  你想为固定尺寸的方阵编写一个template,并支持求逆:

template<typename T, std::size_t n> //此处n即非类型template参数!!!
class SquareMatrix {
public:
...
void invert();
};
使用时:
SquareMatrix<double, > sm1;
...
sm1.invert(); //调用SquareMatrix<double, 5> ::invert SquareMatrix<double, > sm2;
...
sm2.invert(); //调用SquareMatrix<double, 10> ::invert

这就造成代码膨胀,∵会具现化两份invert!!!两份除了操作的是不同大小的矩阵外其他完全相同!!!

解决方案:

template<typename T>     //与尺寸无关的base class
class SquareMatrixBase {
protected:
...
void invert( std::size_t matrixSize ); //尺寸变为了函数参数
}; template<typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> { private:
using SquareMatrixBase<T>::invert; //using声明避免遮掩继承来的名称 public:
...
void invert() {
this->invert(n); //这其实是一个隐晦的inline调用,用this->是为了指涉模板化基类内的成员,这在条款43中讲到
}
};

注意这个继承关系是private:反映此处的base class仅是为了帮助derived class实现。

------------------------------------------

到此设计还没问题,还还有一棘手问题:SquareMatrixBase::invert如何知道该操作什么数据呢?

解决方法:

  令SquareMatrixBase贮存一个指针,指向矩阵数值所在的内存:

template<typename T>
class SquareMatrixBase {
protected:
SquareMatrixBase( std::size_t n, T *pMen ) : size(n), pData( pMem ){
//存储矩阵大小和一个指针,指向矩阵数值
} void setDataPtr( T *ptr ) {
pData = ptr; //重新赋值给pData
}
...
private:
std::size_t size; //矩阵大小,现设计成了class成员变量
T *pData;     //指针,指向矩阵内容
};

这要求derived class决定内存分配方式,有两种分配方式:

方式一:将矩阵数据直接存储在SquareMatrix对象的内部:

template<typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> { SquareMatrix(): SquareMatrixBase<T>( n, data ) { } //用ctor将数据传入
... private:
T data[n*n]; //将矩阵数据存在SquareMatrix对象内部,在栈中
};

方式二:把每个矩阵的数据放进heap:

template<typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> { SquareMatrix(): SquareMatrixBase<T>( n, 0 ), pData( new T[n*n] ) { //用特定函数将数据放入!(与方法一对比)
this->setDataPtr( pData.get() ); //用this->指涉模板化基类内的成员
}
... private:
boost::scoped_array<T> pData; //sp防止内存泄漏
};

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

记住:

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

★若你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy constructor和copy assignment operator。

------------------------------------------------------------------------------------------------

在类的继承体系中,对象指针一直在为我们做一件很好的事情:支持隐式转换。即Derived class指针可以隐式转换为base class指针,"指向non-const对象"的指针可以转换为"指向const对象"等,如下所示:

    class Top{ ... };

  class Middle : public Top{...};

  class Bottom : public Middle{...};

  Top* top1 = new Middle;        //将Middle*转换为Top*

  Top* top2 = new Bottom;        //将Bottom*转换为Top*

  const Top* const_top2 = top1;   //将Top*转换为const Top*

条款13中提到,像std::auto_ptr和tr1::shared_ptr这样的sp,能提供原始指针没有的机能。如能够在正确的时机自动删除heap-based资源。本款中我们自然就想到了,若sp也能支持上述的隐式操作转换,那岂不是很方便。于是我们在这里试图让类似下面的代码通过编译:

 template <typename T>
class SmartPtr{
public:
explicit SmartPtr(T* realPtr); //智能指针通常以原始指针完成初始化
...
};
SmartPtr<Top> top1_smart_ptr = SmartPtr<Middle>(new Middle);
SmartPtr<Top> top2_smart_ptr = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> const_top2_ptr = top1_smart_ptr;

现实情况情况是:同一个template的不同具现体之间并不存在固有的联系。即编译器视SmartPtr<Middle>和SmartPtr<Top>为完全不同的classes

  

  在上述继承体系中,每一条语句都创建一个新式sp对象,我们自然就想到了,我们应该把工作的焦点放在如何编写智能指针的ctor上,使其满足我们的转型需要。而仔细观察,即有一个新的顾忌:永远无法写出我们需要的构造函数。因为继承基类的子类可以有很多个,每诞生一个新的子类,我们就必须要在基类智能指针中添加一个为实现其向子类转换的新的构造函数。那样的代码不仅理解起来很晦涩,更难以维护。在如此"绝境"之下我们想到了模板成员函数,即为SmartPtr写一个ctor模板

 template <typename T>
class SmartPtr{
public:
template <typename U>
SmartPtr( const SmartPtr<U>& other ); //也叫泛化copy ctor
...
};

此ctor并无explicit修饰,故意。因为要完成原始指针之间的隐式转换,我们需要支持这样的操作。若SmartPtr也像auto_ptr和tr1::shared_ptr一样,提供一个get成员函数去发挥智能指针对象的原始指针副本。上面的代码我们可以写的更清楚一点:

template <typename T>
class SmartPtr{
public:
template <typename U>
SmartPtr( const SmartPtr<U>& other )
:held_ptr_( other.get() ){...} //用other的held_ptr_初始化this的held_ptr_,这里
                           //的other的原始对象如果是this原始对象的子类
                           //的话,这里就完成子类向父类的隐式转换过程.
T* get() const { return held_ptr_; }
...
private:
T* held_ptr_; //这是SmartPtr持有的内置指针.
};

成员函数模板的效用不限于ctor,其常扮演的另一个角色是支持赋值操作。这点在TR1的shared_ptr中获得了绝好的发挥。下面是TR1规范中关于tr1::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 ); //或shared_ptr template<class Y>
explicit shared_ptr( weak_ptr<Y> const& r ); //或weak_ptr template<class Y> //或auto_ptr
explicit shared_ptr(auto_ptr<Y>& r); //为啥这里不要const? ∵当你复制一个 //auto_ptr,它们其实被改动了.
template<class Y>
shared_ptr& operator=( shared_ptr<Y> const& r ); template<class Y>
shared_ptr& operator=( auto_ptr<Y>& r ); //为什么这里不要const? 原因同上
...
};

还有点要注意:member template并不会改变语言规则。在前面我们曾提到,若程序需要一个copy构造函数,你却未声明它,编译器会为你暗自生成一个。在class内申明泛化copy构造函数(member template)并不会阻止编译器生成它们自己的copy构造函数(non-template),故你想要控制构造的方方面面,你必须同时声明泛化copy构造和普通copy构造。赋值操作也是一样。下面的tr1::shared_ptr的定义摘要就证明了这点:

template<class T>
class shared_ptr{
public:
shared_ptr( shared_ptr const& r ); //普通copy 构造函数 template<class Y>
shared_ptr( shared_ptr<Y> const& r ); //泛化的copy构造 shared_ptr& operator=( shared_ptr const& r ); //普通的copy assignment template<class Y>
shared_ptr& operator=( shared_ptr<Y> const& r ); //泛化copy assignment
...
};

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

记住:

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

-----------------------------------------------------------------------------------

条款24:唯有non-member函数才有能力“在所有实参身上实施隐式类型转换”!!

将条款24的例子扩充到template情况:

template< typename T>
class Rational {
public:
Rational( const T &numerator = , const T &denominator = );
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 );

Rational<int> result = oneHalf * 2; //编译错误!而条款24的例子却可以,不同之处在于此处的Rational是template!

原因分析因为要推导出T是什么!!通过operator*的第一个实参oneHalf倒确实可以推出T为int。但对第二个实参2就不行了。你或许会期盼编译器使用Rational<int>的non-explicit构造函数将2转化为Rational<int>,进而将T推导为int,但它们不那么做,∵在template实参推导过程中从不将隐式类型转换函数纳入考虑!!!

解决办法:

基于事实:template class内的friend声明式可以指涉某个特定函数!!

template< typename T>
class Rational {
public:
friend const Rational operator*( const Rational &lhs, const Rational &rhs ); //声明!!
...
};
template< typename T >
const Rational<T> operator*( const Rational<T> &lhs, const Rational<T> &rhs ) {...} //定义!!

这时Rational<int> result = oneHalf * 2; 就能编译(仅仅是在template class内加了一个非成员友元的声明!!!好神奇!!!)。因为当对象oneHalf被声明为一个Rational<int>,于是class Rational<int>被具现化了,而作为过程的一部分,friend函数operator*(接受Rational<int>参数)也就被自动声明出来了!!!

还存在问题上述代码能编译,但不能链接!!!!!!!

原因就在于上述的operator*的声明和定义不能分开!!!!!!!!!!!!!!!!!!

解决办法是将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.denomonator()
);
} //定义在class的声明式中也即给出!!!
...
};

这项技术的趣味点:即friend特性的新用途!!!

  此处的friend的用法已经不是传统的用法(为了能访问类的私有成员)。

  为了让给类型转换可能发生于所有实参,我们需一个non-member函数;

  为了令这个non-mem函数被自动具现化,需将其声明在class内部;

  而在class内部声明non-member函数的唯一办法就是用friend!!!!

----------------------------------------

上述定义在class template内的非成员 friend函数还可调用定义于class外部的辅助函数:

(当然本例要不要这么搞无所谓,因为operator*内本来就只有一行代码)

template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs,
const Rational<T>& rhs);
template<typename T>
class Rational{
public:
...
friend const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs){
return doMultiply( lhs, rhs ); //令friend调用外部辅助函数
}
...
}; template<typename T> //外部辅助函数
const Rational<T> doMultiply (const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}