【C++】《Effective C++》第七章
第七章 模板与泛型编程
条款41:了解隐式接口和编译期多态
面向对象设计中的类(class
)考虑的是显式接口(explict interface
)和运行时多态,而模板编程中的模板(template
)考虑的是隐式接口(implict interface
)和编译器多态。
请记住
classes
和templates
都支持接口(interfaces
)和多态(polymorphism
)。- 对
classes
而言,接口是显式的(explict
),以函数签名为中心。多态则是通过virtual
函数发生于运行期。 - 对
template
而言,接口是隐式的(implict
),基于有效表达式。多态则是通过template
具现化和函数重载解析(function overloadding resolution
)发生于编译器。
条款42:了解typename
的双重意义
在template
声明式中,class
和typename
有什么不同?
template<class T> class Widget;
template<typename T> class Widget;
答案是没有什么不同,当我们声明template
类型参数,class
和typename
的意义完全相同。
然而C++
并不总是把class
和typename
视为等价,有时一定得使用typename
。
#include <iostream>
#include <vector>
template<typename C>
void printTwoValue(const C& container) { // 打印容器内的第二个元素
if(container.size() >= 2) {
C::const_iterator iter(container.begin()); // Error
// typename C::const_iterator iter(container.begin()); // Success
// auto iter(container.begin()); // Success
++iter;
int value = *iter;
std::cout << value << std::endl;
}
}
int main(int argc, char* argv[]) {
std::vector<int> vec{1, 2, 3, 4};
printTwoValue(vec);
return 0;
}
#include <iostream>
#include <vector>
template<typename C>
void printTwoValue(const C& container) { // 打印容器内的第二个元素
if(container.size() >= 2) {
// C::const_iterator iter(container.begin()); // Error
// typename C::const_iterator iter(container.begin()); // Success
auto iter(container.begin()); // Success
++iter;
int value = *iter;
std::cout << value << std::endl;
}
}
int main(int argc, char* argv[]) {
std::vector<int> vec{1, 2, 3, 4};
printTwoValue(vec);
return 0;
}
一般情况下,任何时候当你想要在template
中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename
。但是typename
只被用来验证嵌套从属类型名称。其他名称不该有它存在。
templare<typename C>
void f(const C& container, typename C::iterator iter);
但是这一个规则有一个例外:typename
不能出现在base classes list
内的嵌套从属类型名称之前,也不可在member initialization list
(成员初始值列表)中作为base class
修饰符。
tempalate<typename T>
class Derived: public Base<T>::Nested { // base classes list中不允许"typename"
public:
explicit Derived(int x): Base<T>::Nested(x) { //
typename Base<T>::Nested temp;
//
}
};
请记住
- 声明
template
参数时,前缀关键字class
和typename
可互换。 - 请使用关键字
typename
标识嵌套从属类型名称,但是不可以在base class list
(基类列)或member initialization list
(成员初始值列表)内以它们作为base class
修饰符。 - 最重要的,当你不确定是否应该使用
typename
时,使用C++11
新标准auto关键字
最为稳妥。
条款43:学习处理模板化基类内的名称
假设以下MsgSender
类可以通过两种方式发送信息到各个公司:
template<typename Company>
class MsgSender {
public:
// ...
// 1. 发送原始文本
void sendClear(){
Company c;
c.sendClearText();
}
// 2. 发送加密后的文本
void sendSecret(){
// ...
}
};
假设我们有时候想要每次发出信息时记录某些信息,因此有了如下派生类:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
// ...
void sendClearMsg() {
// 将传送前的信息写入日志
sendClear(); // Error,因为不知道继承什么样的类,Company是个template参数
// 将传送后的信息写入日志
}
// ...
};
现在的问题是,如果有一个公司CompanyZ
支持加密传送,那么泛化的MsgSender
就不合适了,所以需要产生一个特例化版的MsgSender
:
template<>
class MsgSender<CompanyZ> {
public:
// 只支持发送加密后的文本
void sendSecret(){
// ...
}
};
当base class
被指定为MsgSender
时,其内不包含sendClear
方法,那么dervied class LoggingMsgSender
的sendClearMsg
就会调用不存在的sendClear
方法。
正是因为知道base class templates
有可能被特例化,而且特例化版本不提供和一般性template
相同的接口,因此C++往往拒绝模板化基类(templatized base classes
)。
解决这个问题的办法有三个,它们会通知编译器进入base class
作用域查找继承而来的名称。
- 方法1:使用
this->
:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
// ...
void sendClearMsg() {
// 将传送前的信息写入日志
this->sendClear(); // Success,假设sendClear被继承
// 将传送后的信息写入日志
}
// ...
};
- 方法2:使用
using
:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
using MsgSender<Company>::sendClear(); // 告诉编译器,让她假设sendClear位于base class内
// ...
void sendClearMsg() {
// 将传送前的信息写入日志
sendClear(); // Success,假设sendClear被继承
// 将传送后的信息写入日志
}
// ...
};
- 方法3:通过作用域明确指出:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
// ...
void sendClearMsg() {
// 将传送前的信息写入日志
MsgSender<Company>::sendClear(); // Success,假设sendClear被继承
// 将传送后的信息写入日志
}
// ...
};
方法3有一个问题存在,假如被调用的是virtual
函数,这样会关闭"virtual
函数的绑定行为"。
请记住
- 可在
derived class templates
内通过"this->
"指涉base class template
内的成员名称,或由一个明白写出的"base class资格修饰符
"完成。
条款44:将与参数无关的代码抽离templates
模板提供的是编译期的多态,即使你的代码看起来完全简短,生成的二进制文件也可能包含大量冗余代码。把模板中参数无关的代码重构到模板外便可以有效地控制模板产生的代码膨胀。
- 对于非类型模板参数而造成的代码膨胀,用函数参数或成员变量来替换模板参数即可消除冗余:
// 非类型模板参数而造成的代码膨胀
template<typename T, std::size_t n>
class SquareMatrix {
public:
// ...
void invert(); // 求逆矩阵
};
SquareMatrix<double, 5> s1;
SquareMatrix<double, 10> s2;
s1.invert();
s1.invert();
// 使用函数参数消除重复
template<typename T>
class SquareMatrixBase {
protected:
// ...
void invert(); // 求逆矩阵
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase {
private:
using SquareMatrixBase<T>::invert();
public:
// ...
void invert() {
this->invert();
}
};
- 对于类型参数而造成的代码膨胀,可以让不同实例化的模板类共用同样的二进制表示:
int
和long
在多数平台都是一样的底层实现,然而模板却会实例化分为两份,因为它们类型不同。List<int>
,List<const int>
,List<double*>
的底层实现也是一样的,但是因为指针类型不同,也会实例化为多份模板类,如果某些成员函数操作强型指针(untyped pointers
,即void*
)的函数,应该令它们调用另一个操作无类型指针(void*
)的函数,后完成实际工作。
请记住
Templates
生成多个classes
和多个函数,所以任何template
代码都不该与某个造成膨胀的template
参数产生相依关系。- 因非类型模板参数(
non-type template parameters
)而造成的代码膨胀,往往可消除,做法是以函数参数或class
成员变量替换template
参数。 - 因类型参数(
type parameters
)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations
)的具现类型(instantiation types
)共享实现码。
条款45:运用成员函数模板接受所有兼容类型
假设SmartPtr
是一种智能指针,并且它是一个template class
,现在有一个继承体系:
class Top{};
class Middle: public Top {};
class Bottom: public Top {};
现在希望通过一个SmartPtr<Bottom>
或SmartPtr<Middle>
来初始化一个SmartPtr<Top>
,如果是指针,即Middle*
和Bottom*
可以隐式转换为Top*
,问题是:同一个template的不同具现体之间不存在什么与生俱来的固有关系,即使具现体之间具有继承关系。所以,SmartPtr<Bottom>
或SmartPtr<Middle>
并不能隐式转换为SmartPtr<Top>
。
可以用一个构造函数模板来实现这种转换:
template<typename T>
class SmartPtr{
private:
T* heldPtr;
public:
template<typename U>
SmartPrt(const SmartPtr<U> &other): heldPtr(other.get()){}
T* get() cosnt {
return heldPtr;
}
};
请记住
- 请使用member function templates(成员函数模板)生成"可接受所有兼容类型"的函数。
- 如果你声明member templates用于"泛化copy构造"或"泛化assignment操作",你还是需要声明正常的copy构造函数和copy assignment操作符。
条款46:需要类型转换时请为模板定义非成员函数
有如下实现:
template<typename T>
class Rational{
public:
Rational(const T&numberator=0, const T&denominator=1);
const T numberator() const;
const T denominator() const;
// ...
};
template<typename>
const Rational<T> operator*(const Rational<T>&lhs, const Rational<T>&rhs){
// ...
}
// 使用
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; // 编译错误
分析:将oneHalf
传递给operator*
时,它将T
推断为int
,因此期待第二个参数也为Rational
,但是第二个参数为int
,由于,C++
中template
实参推导过程中从不将隐式类型转换函数纳入考虑,所以编译错误。
请记住
- 当我们编写一个
class template
,而它所提供之"与此template
相关的"函数支持"所有参数之隐式类型转换"时,请将那些函数定义为"class template
内部的friend
函数"。
条款47:请使用traits classes
表现类型信息
traits
并不是C++
关键字或一个预先定义好的构件,它们是一种技术,也是一个C++
程序员共同遵守的协议。
traits
,又被叫做特性萃取技术,说得简单点就是提取“被传进的对象”对应的返回类型,让同一个接口实现对应的功能。因为STL
的算法和容器是分离的,两者通过迭代器链接。算法的实现并不知道自己被传进来什么。萃取器相当于在接口和实现之间加一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(例如,偏特化)。
请记住
Traits classes
使得"类型相关信息"在编译期可用。它们以templates
和"templates特化
"完成实现。- 整合重载技术(
overloading
)后,traits classes
有可能在编译器对类型执行if...else
测试。
条款48:认识template
元编程
Template mateprogramming(TMP)
是编写template-based C++
程序并执行于编译期的过程。Template mateprogram(模板元程序)
是以C++
写成,执行于C++
编译器内的程序。
TMP
的两个重要特点:
- 基于template。
- 编译器执行。
TMP
的两个效力:
- 它让某些事情更容易。如果没有它,有些事情将是困难的,甚至是不可能的。
- 执行于编译期,因此可将工作从运行期转移到编译期,但是会导致以下结果:
- 某些原本在运行期才能侦测到的错误现在可以在编译期中找出来。
- 使用
TMP
的C++
程序可能在每一个方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。 - 编译时间变得更长了。
#include <iostream>
template <unsigned n>
struct Factorial {
enum { value = n * Factorial<n-1>::value };
};
template <>
struct Factorial<0> {
enum { value = 1 };
};
int main(int argc, char* argv[]){
std::cout << Factorial<5>::value << std::endl;
std::cout << Factorial<10>::value << std::endl;
return 0;
}
请记住
TMP
(模板元编程)可将工作由运行期转移到编译期,因而得以实现早期错误侦测和更高的执行效率。TMP
可被用来生成"基于政策选择组合"的客户定制代码,也可以用来避免生成对某些特殊类型并不合适的代码。