条款41 了解隐式接口和编译器多态
(1)面向对象的编程世界总是以显示接口以及运行期多态解决问题。
//显式接口:通常由函数的签名式(函数名称、参数类型、返回类型)构成。
class Widget{
public:
Widget();
virtual ~Widget();
virtual size_t size() const;
virtual void normalize();
void swap(Widget& other);
};
void doProcessing(Widget& w)
{
if (w.size() > 10 && w != somNastyWidget)
{
Widget(tmp);
tmp.normalize();
tmp.swap(w);
}
}
由于w是Widget类型,所以w必须支持Widget接口,可以在源代码中找到所有接口,看看它什么样子,这种接口称为显示接口。
由于Widget的某些显示成员是虚函数,w对那些函数的调用显示将表现出运行期多态。
(2)再来看看模板的做法
//隐式接口:基于有效表达式
//模板的约束就是if语句里的表达式需要成立,表现出隐式接口。
//同样函数调用带来的具现化和函数多态解析发生在编译期,呈现出编译期多态。
template <typename T>
void doProcessing(T& w)
{
if(w.size() >0 && w != somNastyWidget)
{
T temp(w);
temp.normalize();
temp.swap(w);
}
}
(3)总结:
运行期多态:哪一个virtual函数该被绑定 (发生在运行期)
编译器多态:哪一个重载函数该被调用 (发生在编译期)
显式接口:由函数的签名式(函数名,参数类型,返回类型)构成。
隐式接口:由有效的表达式组成,由一些约束组成
classes和template都支持接口和多态。
对classes而言接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期。
对template参数而言,接口是隐式的,基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。
条款42 了解typename的双重意义
(1)
template <class T> 与 template <typename T>没有区别。
(2)
typename 的另一种用法,我们以一个代码来解释:
template<typename C>
void print2nd(const C&container) //C是一种STL
{
if(container.size() >= 2)
{
typename C::const_iterator iter(container.begin());
//C::const_iterator 的名称依赖于模板参数C,是从属于C的名称,同时带了::符号是一个嵌套从属名称,需要在前面加上typename
//这样才能通过编译,不然C::const_iterator 完全可能是一个静态变量。
++iter;
int value = *iter; //int是一个非从属名称
std::cout<<value;
}
}
C::const_iterator 的名称依赖于模板参数C,是从属于C的名称,同时带了::符号是一个嵌套从属名称,需要在前面加上typename
这样才能通过编译,不然C::const_iterator 完全可能是一个静态变量。
下面代码也是一样:
template<typename C> //使用typename 或 class
void f( const C& container, //不能使用typename,不是嵌套从属名称
typename C::iterator iter); //必须使用typename,是嵌套从属名称
(3)
由于 C::const_iter太长我们经常会有这种用法 typedef typename C::const_iter const_iter.
另外 注意不要把typename放在 基类列表 以及 成员初始列 之中。
template<typename T>
class Derived: public Base<T>::Nested { //base class list中不允许 typename
public:
explicit Derived(int x):Base<T>::Nested(x) //成员初始列 中不允许 typename
{
typename Base<T>::Nested temp; //作为基类修饰符必须使用typename,是嵌套从属类型名称
...
}
};
(4)最后一个例子
template<typename IterT>
void work(IterT iter) //IterT 可能是 vector<int>::iterator
{
typename iterator_traits<IterT>::value_type temp(*iter); //iterator_traits<IterT>::value_type是标准的traits class的一种应用(条款47)
... //"类型为IterT之对象所指之物的类型"。
//明显也是一个嵌套从属类型名称,加上typename
}
加一个typedef
template<typename IterT>
void work(IterT iter) //IterT 可能是 vector<int>::iterator
{
typedef typename iterator_traits<IterT>::value_type value_type; //typedef的用法
value_type temp(*iter); //iterator_traits<IterT>::value_type是标准的traits class的一种应用(条款47)
... //"类型为IterT之对象所指之物的类型"。
//明显也是一个嵌套从属类型名称,加上typename
}
条款43 学习处理 模板基类内 的名称
- (1)派生类继承模板基类,调用基类函数的问题。
class CompanyA{
public:
void sendCleartext(string & msg);
void sendEncrypted(string & msg);
...
};
class companyB{
public:
void sendCleartext(string & msg);
void sendEncrypted(string & msg);
...
};
class companyZ{
public:
//void sendCleartext(string & msg); //companyZ不提供sendCleartext函数
void sendEncrypted(string & msg);
...
};
class MsgInfo {...}; //保存信息,以备将来产生信息
template <typename Company>
class MsgSender{
public:
...
void sendclear(const MsgInfo& info)
{
string msg;
Company c;
c.sendCleartext(msg);
}
void sendsecret(const MsgInfo& info)
{
string msg;
Company c;
c.sendEncrypted(msg);
}
};
template <> //象征既不是template也不是标准class,而是特化版的MsgSender template,在template实参是companyZ时被使用。
class MsgSender<companyZ>{ //这就是模板全特化,template MsgSender针对类型companyZ特化了,而且其特化是全面性的。
public: //也就是说类型参数被定为companyZ,再没有其他template参数可供变化
... //只有sendsecret
void sendsecret(const MsgInfo& info)
{
string msg;
Company c;
c.sendEncrypted(msg);
}
};
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>{ //每次发送信息时打日志
public:
将传送前的信息写至log;
void sendclearMsg(const MsgInfo& info) //函数名与基类的函数名不一样,避免遮掩
{
sendclear(info); //不能过编译,编译器看不到sendclear()。编译器是不进入base class作用域内查找的。
//因为编译器在遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的类,无法知道是否有sendclear()函数
}
将传送后的信息写至log;
};
(2) 三种解决办法来通过编译:
1、可在 derived class template 内通过“this->”指涉base class templates 内的成员名称
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
将传送前的信息写至log;
void sendclearMsg(const MsgInfo& info)
{
this->sendclear(info); //成立,假设sendclear()将被继承。
}
将传送后的信息写至log;
};
2、这里的情况是,编译器本来是不进入base class作用域内查找。于是我们通过using告诉它,请他这么做。
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
using MsgSender<Company>::sendClear; //告诉编译器,假设sendclear位于base class内
...
void sendclearMsg(const MsgInfo& info)
{
sendclear(info); //成立,假设sendclear()将被继承。
}
...
};
3、明白写出“base class 的修饰符”完成。
这是最不让人满意的解法,因为如果被调用的是虚函数,MsgSender<Company>::会关闭 virtual绑定行为
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
...
void sendclearMsg(const MsgInfo& info)
{
MsgSender<Company>::sendclear(info); //成立,假设sendclear()将被继承。
}
...
};
(3)上述三种方法都是:对编译器承诺“基类模板的任何特化版本都将支持其一般(泛化)版本所提供的接口”。
这个承诺是编译器在解析像LoggingMsgSender这样的derived class template时需要的。但如果这个承诺最终未被实践出来,编译器还是会报错的。
LoggingMsgSender<companyZ> zMsgSender;
MsgInfo msg;
...
zMsgSender.sendclearMsg(msg); //错误,无法通过编译!companyZ没有sendclear()
条款44 将与参数无关的代码抽离templates
(1)
模板类的函数只有在被使用的时候才被暗中具现化。
在编写函数的时候,我们会把两个函数相同的部分进行抽离形成一个单独的函数以避免带来重复以及函数膨胀。
编写模板类也一样,如果其中有些部分和另一个部分类的一个部分相同,我们就抽离相同的部分,利用复合继承的方式来使用这些抽离的class。
在模板编程中,重复是相对隐晦的,因为只存在一份模板代码。
(2)代码膨胀: template <class T, size_t n> 这种代码会为每一个n生成一个实现码,注意将n先关的代码抽离。
template <typename T, size_t n> //模板接受一个类型参数T,还接受一个类型为size_t的非类型参数
class SquareMatrix{
public:
...
void invert();
};
SquareMatrix<double, 5> sm1;
sm1.invert();
SquareMatrix<double, 10> sm2;
sm2.invert(); //这就具现化了两份invert()。这是template引出代码膨胀的典型例子
(3)避免派生类代码重复的方法:思路是 抽出两个函数重复部分,把他们放到第三个函数中。
template <typename T> //于尺寸无关的基类,用于正方矩阵
class SquareMatrixBase{
protected: //protected是因为只是企图“避免派生类代码重复”的一种方法。除了派生类其他的无法直接用。
...
void invert(size_t matrixsize); //给定尺寸求逆矩阵
...
};
template <typename T, size_t n> //模板接受一个类型参数T,还接受一个类型为size_t的非类型参数
class SquareMatrix: private SquareMatrixBase<T> { //private继承因为是 帮助实现的关系
private:
using SquareMatrixBase<T>::invert; //避免遮掩基类invert(条款33)
public:
...
void invert() //inline调用
{
this->invert(n); //因为模板类继承的派生类是不会到基类中找函数的,需要手动写(条款43)
}
};
(4)再加上操作矩阵数据:
template <typename T> //于尺寸无关的基类,用于正方矩阵
class SquareMatrixBase{
protected:
SquareMatrixBase(size_t n, T* pMem): size(n), pData(pMem){}
void setDataPtr(T* ptr)
{pData = ptr;} //重新赋值
...
private:
size_t size;
T* pData; //指向矩阵内容
};
template <typename T, size_t n> //模板接受一个类型参数T,还接受一个类型为size_t的非类型参数
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() : SquareMatrixBase<T>(n, data){} //送出矩阵大小和数据指针
...
private:
T data[n * n];
};
(5)如果对象需要动态分配内存,就把每个矩阵的数据放进heap:
template <typename T> //于尺寸无关的基类,用于正方矩阵
class SquareMatrixBase{
protected:
SquareMatrixBase(size_t n, T* pMem): size(n), pData(pMem){}
void setDataPtr(T* ptr)
{pData = ptr;} //重新赋值
... //其他的 派生类 都可以调用的函数.
private:
size_t size;
T* pData; //指向矩阵内容
};
template <typename T, size_t n> //模板接受一个类型参数T,还接受一个类型为size_t的非类型参数
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() : SquareMatrixBase<T>(n, 0), pData(new T[n*n]) //先将基类数据指针设为null,再为矩阵分配内存
{
this->setDataPtr(pData.get()); //因为模板类继承的派生类是不会到基类中找函数的,需要手动写(条款43)
//将pData的一个副本交给base class
}
...
private:
boost::scoped_array<T> pData; //智能指针,管理动态数组内存的。关于boost::scoped_array<T> 见条款13
};
(6)其他:
templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
因非类型模板参数而造成的代码膨胀,可消除。方法是以函数参数或class成员变量替换template参数。
因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码。
条款45 运用成员函数模板接受所有兼容类型
(1)
原始指针之间存在这样派生类向基类的转换,也就是一种隐式转换关系:类似下面这种情况:
class Top{};
class Middle: public Top{};
class Bottom: public Middle{};
Top* pt1 = new Middle; //真实指针 支持隐式转换
Top* pt2 = new Bottom;
const Top* pct2 = pt1; //Top* 转换为 const Top*
我们也希望智能指针支持类似这样的操作,希望下面的代码通过编译:
template<typename T>
class SmartPtr{
public:
explicit SmartPtr(T* realPtr); //原始指针完成初始化
};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle); //希望这几个能通过编译
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Bottom); //本来这几个具现体是没有关系的。
SmartPtr<const Top> pct2 = pt1;
(2)Template 和 泛型编程
方式一:写一个泛化copy构造函数,但是会发生 根据SmartPrt<Top>创建SmartPrt<Bottom>,这不是我们想看到的。
template<typename T>
class SmartPtr{
public:
explicit SmartPtr(T* realPtr); //原始指针完成初始化
template<typename U>
SmartPtr(const SmartPtr<U>& other); //泛化copy构造函数 不是explicit,因为支持隐式转换
};
方式二:加一个原始指针 约束 转换行为。
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other): heldPrt(other.get()) //以other的heldPrt初始化this的heldPrt
{...}
T* get() const
{return heldPtr;}
private:
T* heldPtr; //SmartPtr的原始指针。这有U可以隐式转换到T,才能通过编译
};
(3)成员函数模板效用不限于构造函数,还有赋值操作。例如:
template<class T>
class shared_ptr{
public:
shared_ptr(shared_ptr const& r); //copy构造函数 也要有。
template<class Y>
explicit shared_ptr(Y* p);
//泛化copy构造函数
template<class Y>//这个没有explicit说明 从某个shared_ptr类型隐式转换为另一个shared_ptr是允许的
shared_ptr(shared_ptr<Y> const& r);
template<class Y>
explicit shared_ptr(weak_ptr<Y> const& r); //从其他内置指针或智能指针 隐式转换 是不允许的。
shared_ptr& operator=(shared_ptr const& r); //copy赋值操作符也要有
template<class Y> //泛化copy赋值操作符
shared_ptr& operator=(shared_ptr<Y> const& r); //赋值,来自任何兼容的shared_ptr或auto_ptr
};
这里主要涉及的两个点:
1.使用成员函数模板来生成可接受所有兼容类型的函数。
2.如果声明了成员函数模板用于泛化copy构造函数,或泛化拷贝赋值函数,请注意声明正常的copy函数和拷贝赋值函数
(4)
对普通函数来说,声明放在头文件中,定义放在源文件中,其它的地方要使用该函数时,仅需要包含头文件即可,因为编译器编译时是以一个源文件作为单元编译的,当它遇到不在本文件中定义的函数时,若能够找到其声明,则会将此符号放在本编译单元的外部符号表中,链接的时候自然就可以找到该符号的定义了。
对于模板。先明确,模板函数是在编译器遇到使用模板的代码时才将模板函数实例化(之前只有函数的模板定义并没有根据函数的使用实现其具体定义)的。若将模板函数声明放在tem.h,模板定义放在tem.cpp,在main.cpp中包含头文件,调用add,按道理说应该实例化int add(int,int)函数,即生成add函数的相应代码,但是此时仅有声明,找不到定义(这里就是需要生成函数的定义),因此此时,它只会实例化函数的符号,并不会实例化函数的实现,即这个时候,在main.o编译单元内,它只是将add函数作为一个外部符号,这就是与普通函数的区别,对普通函数来说,此时的add函数已经由编译器生成相应的代码了,而对模板函数来说,此时并没有生成add函数对应的代码。此时编译main.cpp单元不会报错,但链接就会出现add函数未定义的错误。
其实很明显,明确一点就可以了,即编译器只要遇到使用模板函数时就会实例化相应的函数,若在此编译单元内没有模板函数的定义,它当然不能够实例化成功了。因此通常情况下模板函数的声明与定义均放在同一文件内,因此这样就保证了在使用模板的地方一定可以实例化成功了。
条款46 需要 类型转换 时请为模板定义 非成员函数
(0)
需要类型转换时,请为模板定义class 内部的friend函数
(1)还是以一个例子作为开头:
template <typename T>
class Rational{
public:
Rational(const T& numerator = 0, //传递引用比较高效
const T& denominator = 1); //没有explicit,支持隐式转换
const T numerator() const; //返回const,避免暴露接口
const T denominator() const;
...
};
template <typename T>
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs)
{...}
int main()
{
Rational<int> onefour(1, 4);
Rational<int> ret = onefour * 2; //无法通过编译。说明模版化后是不一样的。编译器不知道我们要调用哪个函数
return 0; //因为在template的实参推导中不会将隐式转换函数纳入考虑
}
(2)我们可以利用 类不用进行函数推导的性质声明并定义个friend内部函数。
当我们编写class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,
请将那些函数定义为“class template”内部的friend函数。
template <typename T>
class Rational{
public:
Rational(const T& numerator = 0, //传递引用比较高效
const T& denominator = 1); //没有explicit,支持隐式转换
const T numerator() const; //返回const,避免暴露接口
const T denominator() const;
friend //为了让类型转换可以发生在所有实参身上,必须要非成员函数
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) //为了令这个函数被自动具现化,我们需要将它声明在class内部。所以只能是friend
{
return Rational<T>( lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
};
int main()
{
Rational<int> onefour(1, 4);
Rational<int> ret = onefour * 2; //可通过编译
return 0;
}
(3)friend是个inline函数。所以又新做法:让friend调用辅助函数
template <typename T>
class Rational; //声明类
template <typename T> //声明helper函数
const Rational<T> doMultiply(const Rational<T>& lhs,
const Rational<T>& rhs);
template <typename T>
class Rational{
public:
Rational(const T& numerator = 0, //传递引用比较高效
const T& denominator = 1); //没有explicit,支持隐式转换
const T numerator() const; //返回const,避免暴露接口
const T denominator() const;
friend
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
return doMultiply(lhs, rhs);
}
};
template <typename T> //声明helper函数
const Rational<T> doMultiply(const Rational<T>& lhs,
const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
条款47 请使用traits classes表现类型信息
(0)
Traits是一种技术,也是一个c++程序员共同遵守的协议。
traits classes使得类型相关信息在编译期可用。他们以template以及templates 特化完成实现。
整合重载技术后,traits classes有可能在编译期对类型执行If..else...测试.
(1)以标准库中的迭代器为例:
首先根据迭代器的类型为迭代器提供卷标分类。
- struct input_iterator_tag{}; //一次一步,只能读一次所指的东西。 istream_iterator
- struct output_iterator_tag{}; //一次一步,只能写一次所指的东西。ostream_iterator
- struct forward_iterator_tag:public input_iterator_tag{}; //一次一步,读或写其所指物一次以上
- struct bidirectional_iterator_tag:public forward_iterator_tag {}; //前后移动,一次一步。list、set、map迭代器
- struct random_access_iterator:public bidirectional_tag{}; //前后移动、一次n步。vector、deque、string。
(2)举个例子:
template <typename IterT, typename DistT> //将迭代器向前移动d个单位
void advance(IterT& iter, DistT d)
{
if (iter is a random access iterator)
{
iter += d; //针对random access迭代器使用迭代器算术运算
}
else //其他迭代器,多次调用++ --
{
if (d >= 0)
{
while (d--) iter++;
}
else
{
while (d++) iter--;
}
}
}
这种做法首先必须判断iter的类型。
(3)Traits:允许你在编译期间取得某些类型信息
这种技术手法有两个要求
1、每一个用户自定义类型必须嵌套一个typedef名为iterator_category,用来确定适当的卷标结构。
2、为了支持指针迭代器,iterator_traits特别针对指针类型提供一个偏特化版本。
template<...>
class deque
{
public:
class iterator
{
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
template<...>
class list
{
public:
class iterator
{
public:
typedef bidirectional_iterator_tag iterator_category;
...
};
...
};
template <typename IterT> //template用来处理迭代器分类的相关信息
struct iterator_traits{
typedef typename IterT::iterator_category iterator_category; //嵌套从属类型名称 前面加typename
};
template<typename IterT> //template偏特化针对内置指针
struct iterator_traits<IterT*>
{
typedef random_access_iterator_tag iterator_category;
};
如何实现一个traits class:
1、确认若干你希望将来可取得的类型信息。例如对迭代器而言,我们希望将来可取得其分类(category)。
2、为该信息选择一个名称(iterator_category)
3、提供一个template和一组特化版本,内含你希望支持的类型相关信息。
(4)现在有了iterator_trait,我们重写advance
template <typename IterT, typename DistT> //将迭代器向前移动d个单位
void advance(IterT& iter, DistT d)
{
if (typeid(typename iterator_traits<iter>::iterator_category) ==
typeid(random_access_iterator_tag))
{
iter += d; //针对random access迭代器使用迭代器算术运算
}
else //其他迭代器,多次调用++ --
{
if (d >= 0)
{
while (d--) iter++;
}
else
{
while (d++) iter--;
}
}
}
//上面的代码是过不了编译的。因为编译器会确保所有码源有效,当iter不是random时,也会试一试iter += d; 这个时候比如说list<int>::iterator就肯定过不了。
//考虑另外的事,if语句在运行期才会核定,有没有编译期核定的办法!有!重载!
(5)上面的代码可以使用重载:
template <typename IterT ,typename DistT> //重载函数(劳工)
void doadvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{
iter += d;
}
template <typename IterT ,typename DistT> //重载函数(劳工)
void doadvance(IterT& iter, DistT d, std::input_iterator_tag)
{
if (d >= 0)
while (d--) iter++;
else
while (d++) iter--;
}
template <typename IterT, typename DistT> //将迭代器向前移动d个单位
void advance(IterT& iter, DistT d) //控制函数(工头)
{
doadvance( //调用的doadvance版本对于iter迭代器而言必须是适当的
iter, d,
typename iterator_traits<IterT>::iterator_category() //嵌套从属名称 加 typename
); //获取trait参数,也就是类型相关信息
}
(6)总结:如何使用一个trait class:
1、建立一组重载函数(身份像劳工)或函数模板(doadvance),彼此间的差异只在于各自的trait参数。令每个函数实现码与其接受之traits信息相应和。
2、建立一个控制函数(工头)或函数模板(advance),它调用上述那些劳工函数并传递traits class所提供的信息。
条款48 认识template元编程
(1)
模板元编程是编写template-based c++程序并执行于编译期的过程。
模板元编程可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
(2)重新看下面这个代码:
template <typename IterT, typename DistT> //将迭代器向前移动d个单位
void advance(IterT& iter, DistT d)
{
if (typeid(typename iterator_traits<iter>::iterator_category) ==
typeid(random_access_iterator_tag))
{
iter += d; //针对random access迭代器使用迭代器算术运算
}
else //其他迭代器,多次调用++ --
{
if (d >= 0)
{
while (d--) iter++;
}
else
{
while (d++) iter--;
}
}
}
1、由于类型测试发生在运行期而非编译期,所以这个比trait版本慢很多。
2、这个代码会编译失败,因为编译器会确保所有码源有效,当iter不是random时,也会试一试iter += d; 这个时候比如说list<int>::iterator就肯定过不了。
3、而trait-based TMP解法,其针对不同类型而进行的代码,被拆分成不同函数,每个函数所使用的操作都可使用于该函数所对付的类型。
(3) TMP实现 递归模板具现化
template <unsigned n>
struct Factorial{
enum{ value = n * Factorial<n - 1>::value};
};
template<> //全特化版本
struct Factorial<0>{
enum{ value = 1};
};
//Factorial<n>::value 就可以得到n阶乘
(4)TMP可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊处理类型并不适合的代码。