第七章:模板与泛型编程
条款41:了解隐式接口和编译期多态
-
编译期的多态和运行期的多态的区别:
- 哪一个重载函数该被调用:发生在编译期
- 哪一个virtual函数该被绑定:发生在运行期
-
显式接口与隐式接口的区别:
- 显式接口由函数的签名式(函数名称、参数类型、返回类型)构成。如:
class Widget{ public: Widget(); virtual ~Widget(); virtual std::size_t size() const; virtual void normalize(); void swap(Widget& other); }
- 隐式接口并不基于函数签名式,而是由有效表达式组成。
template<typename T> void doProcessing(T& w) { if(w.size() > 10 && w!=someNastyWidget){ ... } }
T的隐式接口有如下约束:
1.它必须提供一个名为size的成员函数,该函数返回一个整数值。
2.它必须支持一个operator!=函数,用来比较两个T对象。这里我们假设someNastyWidget的类型为T -
小结:
- 对于class而言,接口是显式的,以函数签名为中心。多态则是通过virtual 函数发生于运行期。
- 对template参数而言,接口是隐式的,基于有效表达式。多态则是通过template具现化和函数重载解析,发生于编译期。
条款42:了解typename的双重意义
- 当我们声明template类型参数,class和typename的意义完全相同。
template<class T> class Widget;
template<typename T> class Widget;
- 使用关键字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 << value;
}
}
-
从属名称:template内出现的名称如果相依于某个template参数,称之为从属名称。如C::const_iterator实际是什么必须取决于template C.
-
如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称。
-
非从属名称:并不依赖于任何template参数的名称。如:value 的值是int.
-
C++有个规则:如果解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是个类型,除非你告诉它是(此时便需要使用typename)
//上述代码中修改
typename C::const_iterator iter(container.begin());
- 自动决定类型
template<typename IterT>
void workWithIterator(IterT iter)
{
typename std::iterator_traits<IterT>::value_type temp(*iter);
...
}
条款43:学习处理模板化基类内的名称
模板类之间的继承
//基类
template<typename Company>
class MsgSender{
public:
void sendClear(){...}
...
};
//子类
template<typename Company>
class Logging:public MsgSender<Company>{
public:
void sendClaerMsg()
{
sendClear(); //调用基类函数:这段代码无法通过编译
}
...
}
无法通过编译原因:编译器会认为sendClear不存在。在编译时,无法知道Company是什么,那么便无法确定class MsgSender是什么,所以便无法知道是否有个sendClear()函数。
解决办法:
- 在base class 函数调用动作之前加上this->
template<typename Company>
class Logging:public MsgSender<Company>{
public:
void sendClaerMsg()
{
this->sendClear(); //调用基类函数:这段代码无法通过编译
}
...
}
- 使用using声明式
template<typename Company>
class Logging:public MsgSender<Company>{
public:
using MsgSender<Company>::sendClear;
void sendClaerMsg()
{
sendClear(); //调用基类函数:这段代码无法通过编译
}
...
}
- 明白指出被调用的函数位于base class内:
template<typename Company>
class Logging:public MsgSender<Company>{
public:
void sendClaerMsg()
{
MsgSender<Company>::sendClear(); //调用基类函数:这段代码无法通过编译
}
...
}
条款44:将与参数无关的代码抽离templates
template<typename T, std::size_t n>
class SquareMatrix{
public:
...
void invert();
};
SquareMatrix<double, 5> sml;
sml.invert();
SquareMatrix<double, 6> sm2;
sm2.invert();
这会具现化两份invert,这是template引出代码膨胀的一个典型例子。
改善方案:创建一个带数值参数的函数
template<typename T>
class SquareMatrixBase{
protected:
...
void invert(std::size_t matrixSize);
...
};
template<typename T, std::size_t n>
clss SquareMatrix: private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>::invert;
public:
...
void invert(){this->invert(n);}
};
- 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
条款45:运用成员函数模板接受所有兼容类型
- 泛化copy构造函数
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other); //copy构造函数
...
}
class Top{};
class Middle{};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
这一类构造函数根据对象u创建对象t(例如根据SmartPtr< U>创建一个SmartPtr< T>),而U和T的类型是同一个template的不同具现体
如果你想要控制copy构造的方方面面,你必须同时声明copy构造函数和“正常的”copy构造函数。
template<class T>
class shared_ptr{
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 内部的friend函数”
template<typename T>
class Rational{
public:
Rational(const T& numberator=0, const T& denominator=1);
...
};
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; //错误
错误原因:template实参推导过程不将隐式转换函数纳入考虑。onehalf的类型是Rational,所以T一定是int。但对于2,无法推导出T。
解决办法:将operator* 声明为friend函数
template<typename T>
class Rational{
public:
...
friend const Rational operator*(const Rational& lhs, const Rational& rhs)
{
...
}
}
解析:当对象onehalf被声明为一个Rational,class Rational于是被具现化出来,而作为过程的一部分,friend函数operator*接受Rational参数也就被自动声明出来。后者身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数(例如Rational的non-explicit构造函数),而这便是混合式调用之所以成功的原因。
- 小结:friend函数可以隐式转换,而template成员函数不允许隐式转换
条款47:请使用traits classes表现类型信息
- 5种迭代器分类:
-
Input:
Input迭代器只能向前移动,一次一步,客户只可以读取他们所指的东西,并且只能读取一次。 -
Output:
Output迭代器只能向前移动,一次一步,客户只可涂写它们所指的东西,而且只能涂写一次。 -
forward:
可以做前面两种分类所能做的每一件事,而且可以读或写其所指物一次以上。 -
Bidirectional:
比上一个分类威力更大,它除了可以向前移动,还可以向后移动。 -
random access:
可以在常量时间内向前或者向后跳跃任意距离。
条款48:认识template元编程
- 模板元编程TMP:TMP是以C++写成,执行于C++编译器内的程序。
好处:
- TMP执行于C++编译器,因此可以将工作从运行期转移到编译期。这导致一个结果是,某些错误原本通常在运行期才能侦测到,现在可以在编译期找出来。
- 使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件,较短的运行期,较少的内存需求。然后将工作从运行期转移到编译期的另一结果是:编译时间变长了。
例子:
template<unsigned n>
struct Factorial{
enum{ value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0>{
enum {value = 1};
};
//调用
int main()
{
cout << Factorial<5> << endl;
}
用途:
- 确保量度单位正确。使用TMP,就可确保在编译期,程序中所有量度单位的组合都是正确的,不论其计算多么复杂。
- 优化矩阵运算。
- 可以生成客户定制之设计模式。例如在编译期间生成数以百计的智能指针类型。
更细致的讲解:
- 程序执行时在编译期,操纵的数据不能是运行变量,只能是编译期常量,不可以修改。
- 不能使用if-else for(if-else使用type_traits代替;for使用递归和魔板特化实现)
- 由元数据和元函数组成
元数据:编译常量,不能修改。例:enum枚举常量,静态常量,基本类型,自定义类型
元函数:处理元数据的构件