Effective C++ 下
条款41:了解隐式接口和编译期多态
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的任何函数调用,例如operator>和operator!,有可能造成template具现化(instantiated) ,使这些调用得以成功。这样的具现行为发生在编译期。“以不同的template参数具现化function templates"会导致调用不同的函数,这 便是所谓的编译期多态(compile-time polymorphism) 。
显式接口和隐式接口的差异
显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。
- 其public接口由一个构造函数、一个析构函数、函数及其参数类型、返回类型、常量性(constnesses)构成。当然也包括编译器产生的copy 构造函数和copy assignment操作符(见条款5) 。
隐式接口就完全不同了。它并不基于函数签名式,而是由有效表达式(valid expressions)组成。
template<typename T>
void doProcessing( T& w)
{
if (w.size() > 10 && w != someNastywidget) {
...
}
}
- 它必须提供一个名为size的成员函数,该函数返回一个整数值。
- 它必须支持一个operator!=函数,用来比较两个T对象。这里我们假设 someNastyWidget的类型为T
本文要点
- classes和templates都支持接口(interfaces)和多态(polymorphism) 。
- 对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。
- 对template参数而言,接口是隐式的(implicit) ,奠基于有效表达式。多态则 是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。
条款42: 了解typename的双重意义
template<class T> class widget; //使用"class"
template<typename T> class Widget; //使用"typename"
- C++并不总是把class和typename视为等价。有时候你一定得使用 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参数, 称之为从属名称(dependent names)。如果从属 名称在class内呈嵌套状,我们称它为嵌套从属名称(nested dependent name) 。int是一个不依赖任何template参数的名称,为非从属名称(non-dependent names)。
template<typename C>
void print2nd(const C& container) {
if(container.size() >=2)
C::const_iterator iter(container.begin()); //假设为非类型
}
如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型。
#正确版本
template<typename C>
void print2nd(const C& container) {
//这是合法的CH+代码
if (container.size() >=2) {
typename C::const_iterator iter(container.begin());
}
}
一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename
“typename必须作为嵌套从属类型名称的前缀词”这一规则的例外是, typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在 member initialization list (成员初值列)中作为base class修饰符。
template<typename T>
class Derived: public Base<T>::Nested { //不允许
public:
explicit Derived(int x) : Base<T>::Nested(x) //不允许
{
typename Base<T>::Nested temp; //允许
...
}
...
};
由于std::iterator-traits: :value-type是个嵌套从属类型名 称(value-type被嵌套于iterator-traits之内而IterT是个template参数) ,所以我们必须在它之前放置typename
template<typename IterT>
void workwithiterator(IterT iter)
typename std::iterator traits<IterT>::value type temp (*iter);
本文要点
- 声明template参数时,前缀关键字class和typename可互换。
- 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists (基类 列)或member initialization list (成员初值列)内以它作为base class修饰符。
条款43: 学习处理模板化基类内的名称
class MsgInfo { ... };
template<typename Company>
class MsgSender {
public:
void sendClear(const MsgInfo& info) {
std::string msg;
这儿,根据info产生信息;
Company c;
c.sendcleartext (msg);
}
void sendSecret (const MsgInfo& info) {
...
}
};
template<typenane Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
void sendClearMsg (const MsgInfo& info) {
//将“传送前”的信息写至log;
sendClear (info); //调用base class函数;这段码无法通过编译。
//将“传送后”的信息写至1og;
}
...
};
上面问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知 道它继承什么样的class。当然它继承的是MsgSender,但其中的Company 是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它 是什么。而如果不知道Company是什么,就无法知道class MsgSender看起来像什么–更明确地说是没办法知道它是否有个sendClear函数。
解决办法
template<>
class MsgSender<Companyz> {
public:
...
//删除了sendclear
void sendSecret(const MsgInfo& info) {
...
}
};
注意class定义式最前头的"template<“语法象征这既不是template也不是 标准class,而是个特化版的MsgSender template,在template实参是Companyz时被使用。这是所谓的模板全特化(total template specialization)。
template<typename Company>
class LoggingMsgSender: public MsqSender<Company> {
public:
void sendClearMsg (const MsgInfo& info)
{
//将“传送前”的信息写至log;
sendClear(info); //如果company == Companyz,这个函数不存在。将“传送后”的信息写至log;
//将“传送后”的信息写至log;
}
...
};
它知道base class templates有可能被特化,而那个特化版本可能不提供和一般性template相同的接口。因此它往往拒绝在templatized base classes (模板化基类, 本例的MsgSenderkCompany>)内寻找继承而来的名称(本例的SendClear) 。
为了重头来过,我们必须有某种办法令C++“不进入templatized base classes观察”的行为失效。
有三个办法,第一是在base class函数调用动作之前加上this->
template<typename Company>
class LoggingMsgSender: public MsqSender<Company> {
public:
void sendClearMsq (const MsgInfo& info) {
//将“传送前”的信息写至log;,
this->sendClear(info);
//将“传送后”的信息写至log;
}
...
};
第二是使用using声明式。
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
using MsgSender<Company>::sendClear;
...
void sendClearMsg (const MsgInfo& info)
{
...
sendClear (info);
...
}
...
};
第三个做法是,明白指出被调用的函数位于base class内:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
void sendClearMsg (const MsgInfo& info) {
...
MsgSender<Company>::sendClear(info);
...
}
...
};
但这往往是最不让人满意的一个解法,因为如果被调用的是virtual函数,上述的明确资格修饰(explicit qualification)会关闭"virtual绑定行为”。
从名称可视点(visibility point)的角度出发,上述每一个解法做的事情都相同:对编译器承诺"base class template的任何特化版本都将支持其一般(泛化)版本所提供的接口”。
LoggingMsgSender<Companyz> zMsgSender;
MsgInfo msgData;
...
zMsgSender.sendClearMsg(msgData); //错误
因为在那个点上,编译器知道base class是个template特化版本Msgsender,而且它们知道那个 class不提供sendClear函数,而后者却是sendClearMsg尝试调用的函数。
本文要点
- 可在derived class templates内通过"this->“指涉base class templates内的成员名称,或藉由一个明白写出的"base class资格修饰符”完成。
条款44: 将与参数无关的代码抽离templates
template<typename T, std::size_t n>
class SquareMatrix {
public:
...
void invert(); // 求逆矩阵
}
SquareMatrix<double,5> sml;
...
sm1.invert(); //调用SquareMatrix<double,5>::invert
SquareMatrix<double,10> sm2;
...
sm2.invert();
这会具现两份invert。这些函数并非完全相同,但除了常量5和10,其他部分都相同,这是template引出代码膨胀的一个典型例子。
下面是第一次修改
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;//避免遮掩base版的invert
public:
void invert() {this->invert(n);} //制造一个inline调用,用this->为了不被derived classes的函数名称掩盖
};
带参数的invert位于base class中。和SquareMatrix一样,也是个template,不同的是他只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于给定的元素对象类型,所有矩阵共享同一个(也是唯一一个)SquareMatrixBase class。也将因此而共享这唯一一个class内的invert。
另一个办法是令SquareMatrixBase贮存一个指针,指向矩阵数值所在的内存。而只要它存储了那些东西,也就可能存储矩阵尺寸:
template<typename T>
class SquareMatrixBase{
protected:
SquareMatrixBase(std::size_t n, T* pMem)
:size(n), pData(pMem){}
void setDataPtr(T* ptr){pData = ptr;}
private:
std::size_t size;
T* pData;
};
这允许derived classes决定内存分配方式。某些实现版本也许会将矩阵数据存储在SquareMatrix对象内部:
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
public:
SquareMatrix(): SquareMatrixBase<T>(n, data) {}
private:
T data[n*n];
}
另一种做法是把每个矩阵的数据放进heap:
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
public:
SquareMatrix()
:SquareMatrixBase<T>(n, 0),//base class的数据指针设为null
pData(new T[n*n])//为内容分配内存,将指向该内存的指针存储起来
{
this->setDataPtr(pData.get());//将pData的一个副本交给base class
}
private:
boost::scoped_array<T> pData;
};
本文要点
- Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
- 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
- 因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有 完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。
条款45: 运用成员函数模板接受所有兼容类型
Templates和泛型编程(Generic Programming)
构造模版, 这样的模板(templates)是所谓member function templates (常简称为member templates) ,其作用是为class生成函数
template<typename>
class SmartPtr {
public:
template<typename U>
SmartPtr (const SmartPtr<U>& other);
};
template<typename>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other) : heldPtr (other.get ()) {..}
T* get() const { return heldPtr; }
private:
T* heldptr;
};
使用成员初值列(member initialization list)来初始化SmartPtr之内类型为T*的成员变量,并以类型为U*的指针(由SmartEtrU>持有)作为初值。这个行为只有当“存在某个隐式转换可将一个U*指针转为一个T*指针”时才能通过编译,
成员函数模版也支持赋值操作
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);
template<class Y>
shared_ptr& operator-(auto_ptr<Y>& r);
};
上述所有构造函数都是explicit,惟有“泛化copy构造函数"除外。那意味从某个sharedptr类型隐式转换至另一个sharedptr类型是被允许的,但从某个内置指针或从其他智能指针类型进行隐式转换则不被认可(如果是显式转换如cast强制转型动作倒是可以)。
member templates并不改变语言规则,而语言规则说,如果程序需要一个copy构造函数,你却没有声明它,编译器会为你暗自生成一个。在class内声明泛化copy构造函数(是个member template)并不会阻止编译器生成它们自己的copy构造函数(一个non-template)
所以如果你想要控制copy构造的方方面面,你必须同时声明泛化copy构造函数和“正常的” copy构造函数。
template<class T>
class shared ptr {
public:
shared_ptr(shared_ptr const& r);
template<class Y>
shared_ptr(shared_ptr<Y> const& r);
shared_ptr& operator=(shared_ptr const& r);
template<class Y>
shared_ptr& operator= (shared_ptr<Y> const& r);
};
本文要点
- 请使用member function templates (成员函数模板)生成“可接受所有兼容类型”的函数。
- 如果你声明member templates用于“泛化copy构造"或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
条款46:需要类型转换时请为模板定义非成员函数
条款24的模版化
template<typename т>
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& 1hs, const Rational<T>& rhs)
{ ... }
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2;
- 在template实参推导过程中从不将隐式类型转换函数纳入考虑。
方法一: template class内的friend声明式可以指涉某个特定函数。那意味class Rational可以声明operator*是它的一个friend函数。令Rational class声明适当的operator*为其friend函数,可简化整个问题
template<typename T>
class Rational {
public:
...
friend const Rational operator* (const Rational& 1hs, const Rational& rhs);
};
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs)
{ ... }
当对象oneHalf被声 明为一个Rational, class Rational于是被具现化出来,而作为过程的 一部分, friend函数operator*(接受Rational参数)也就被自动声明出来。后者身为一个函数而非函数模板(function template) ,因此编译器可在调用它时使用隐式转换函数(例如Rational的non-explicit构造函数)
混合式代码通过了编译,因为编译器知道我们要调用哪个函数(就是接受一个Rationalkint>以及又一个Rationalkint>的那个operator * ) ,但那个函数只被声明于Rational内,并没有被定义出来。
方法二: 将operator * 函数本体合并至其声明式内:
template<typename T>
class Rational {
public:
...
friend const Rational operator*(const Rational& 1hs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator())
}
};
“Rational是个template"这一事实意味上述的辅助函数通常也是个template, 所以定义了Rational的头文件代码,很典型地长这个样子:
template<typename T> class Rational; //声明Rational template
template<typename T>
const Rational<T> doMultiply(const Rational<T>& 1hs, const Rational<T>& rhs);
template<typenane T>
class Rational (
public:
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {
return doMultiply(lhs, rhs);
}
};
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());
}
本文要点
- 当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为"class template内部 的friend函数”
条款47: 请使用traits classes表现类型信息
STL迭代器分类(categories)
-
input选代器 只能向前移动,一次一步,客户只可读取(不能涂写)它们所指的东西,而且只能读取一次。
-
Output迭代器 一切只为输出:它们只向前移动,一次一步,客户只可涂写它们所指的东西,而且只能涂写一次。
-
forward迭代器。 这种迭代器可以做前述两种分类所能做的每一件事,而且可以读或写其所指物一次以上。这使得它们可施行于多次性操作算法(multi-pass algorithms) 。
-
Bidirectional迭代器 比上一个分类威力更大: 它除了可以向前移动,还可以向后移动。
-
random access选代器 这种迭代器比上一个分类威力更大的地方在于它可以执行“迭代器算术”,也就是它可以在常量时间内向前或向后跳跃任意距离。
针对这五种分类,C++标准库分别提供专属的"卷标结构”(tag struct)加以区分:
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag:public input_iterator_tag{};
struct bidirectional_iterator_tag:public forward_iterator_tag{};
struct random_access_iterator_tag:public bidirectional_iterator_tag{};
traits允许我们在编译期得到类型的信息。traits并非一个关键字,而是一个编程惯例。
traits的另一个需求在于advance对与基本数据类型也能正常工作,比如char*。所以traits不能借助类来实现, 于是我们把traits放到模板中。比如:
template<typename IterT> // template for information about
struct iterator_traits; // iterator types
terator_traits< IterT>将会标识IterT的迭代器类别。iterator_traits的实现包括两部分:
- 用户定义类型的迭代器
- 基本数据类型的指针
用户类型的迭代器
在用户定义的类型中,typedef该类型支持迭代器的Tag
template< ... > // template params elided
class deque {
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
}:
};
在全局的iterator_traits模板中typedef那个用户类型中的Tag,以提供全局和统一的类型识别。
template<typename IterT>
struct iterator_traits {
typedef typename IterT::iterator_category iterator_category;
};
基本数据类型的指针
为了支持指针迭代器, iterator-traits特别针对指针类型提供一个偏特化版本(partial template specialization) 。
template<typename IterT>
struct iterator traits<IterT*>
{
typedef random access iterator tag iterator category;
...
};
如何设计并实现一个traits class
- 确认若干你希望将来可取得的类型相关信息。例如对迭代器而言,我们希望将 来可取得其分类(category) 。
- 为该信息选择一个名称(例如iterator_category) 。
- 提供一个template和一组特化版本(例如稍早说的iterator-traits) ,内含你希望支持的类型相关信息。
advance的实现
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
typeid(std::random_access_iterator_tag))
...
}
上述实现其实并不完美,至少if语句中的条件在编译时就已经决定,它的判断却推迟到了运行时(显然是低效的)。 在编译时作此判断,需要为不同的iterator提供不同的方法,然后在advance里调用它们。
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
doAdvance( // call the version
iter, d, // of doAdvance
typename std::iterator_traits<IterT>::iterator_category()
);
}
// 随机访问迭代器
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::bidirectional_iterator_tag) {
if (d >= 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
// 输入迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) {
if (d < 0 ) {
throw std::out_of_range("Negative distance"); // see below
}
while (d--) ++iter;
}
总结如何使用一个traits class
- 建立一组重载函数(身份像劳工)或函数模板(例如doAdvance) ,彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受之traits信息相应和。
- 建立一个控制函数(身份像工头)或函数模板(例如advance) ,它调用上述那些“劳工函数”并传递traits class所提供的信息。
本文要点
- Traits classes使得“类型相关信息”在编译期可用。它们以templates和"templates特化”完成实现。
- 整合重载技术(overloading)后, traits classes有可能在编译期对类型执行 if.else测试。
条款48: 认识template元编程
Template metaprogramming (TMP,模板元编程)是编写template-based C++程序并执行于编译期的过程。
**使用TMP有两个好处: **
- 第一,它让某些事情更容易。如果没有它,那些事情 将是困难的,甚至不可能的。
- 第二,由于template metaprograms执行于C++编译期,因此可将工作从运行期转移到编译期。
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
typeid(std::random_access_iterator_tag)){
iter += d;
}
...
}
list<int>::iterator it;
advance(it, 10);
其实上述代码是不能编译的,设想以下advance<list::iterator, int>中的这条语句:
iter += d;
list<int>::iterator
是双向迭代器,不支持+=运算。虽然上述语句不会执行,但编译器不知道这一点。 编译时这条语句仍然会抛出类型错误。
TMP已被证明是个“图灵完全”(Turing-complete)机器
TMP主要是个“函数式语言” (functional language), TMP的递归甚至不是正常种类,因为TMP循环并不涉及递归函数调用,而是涉及“递归模板具现化”(recursive template instantiation) 。
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0> { //Factorial<0> 值为1
enum{ value=1 };
};
int main() {
std::cout << Factorial<5>::value; //印出120
std::cout << Factorial<10>::value; //印出3628800
}
为了更好地理解TMP的重要性,我们来看看TMP能干什么:
- 确保量纲正确。在科学计算中,量纲的结合要始终保持正确。比如一定要单位为”m”的变量和单位为”s”的变量相除才能得到一个速度变量(其单位为”m/s”)。 使用TMP时,编译器可以保证这一点。因为不同的量纲在TMP中会被映射为不同的类型。
- 优化矩阵运算。比如矩阵连乘问题,TMP中有一项表达式模板(expression template)的技术,可以在编译期去除临时变量和合并循环。 可以做到更好的运行时效率。
- 自定义设计模式的实现。设计模式往往有多种实现方式,而一项叫基于策略设计(policy-based design)的TMP技术可以帮你创建独立的设计策略(design choices),而这些设计策略可以以任意方式组合。生成无数的设计模式实现方式。
本文要点
- Template metaprogramming (TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
- TMP可被用来生成“基于政策选择组合” (based on combinations of policychoices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
了解C++内存管理例程的行为。这场游戏的两个主角是分配例程和归还例程(allocation and deallocation routines,也就是operator new和operator delete), 配角是new-handler, 这是当operator new无法满足客户的内存需求时所调用的函数。
operator new和operator delete只适合用来分配单一对 象。Arrays所用的内存由operator new1]分配出来,并由operator delete[]归还(注意两个函数名称中的[])
条款49: 了解new-handler的行为
当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用个客户指定的错误处理函数,一个所谓的new-handler。
namespace std {
typedef void(*new handler) ();
new_handler set_new_handler(new_handler p) throw();
}
//以下是当operator new无法分配足够内存时, 该被调用的函数
void outOfMem(){
std::cout << "Unable to alloc memory";
std::abort();
}
int main(){
std::set_new_handler(outOfMem);
int *p = new int[100000000L];
}
当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。
一个设计良好的new-handler函数必须做以下事情:
- 使更多内存可用;
- 安装一个新的”new-handler”;
- 卸载当前”new-handler”,传递null给set_new_handler即可;
- 抛出bad_alloc(或它的子类)异常;
- 不返回,可以abort或者exit。
std::set_new_handler设置的是全局的bad_alloc的错误处理函数,C++并未提供类型相关的bad_alloc异常处理机制。 但我们可以重载类的operator new,当创建对象时暂时设置全局的错误处理函数,结束后再恢复全局的错误处理函数。
比如Widget类,首先需要声明自己的set_new_handler和operator new:
class Widget{
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void * operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler current;
};
// 静态成员需要定义在类的外面
std::new_handler Widget::current = 0;
std::new_handler Widget::set_new_handler(std::new_handler p) throw(){
std::new_handler old = current;
current = p;
return old;
}
关于abort, exit, terminate的区别:abort会设置程序非正常退出,exit会设置程序正常退出,当存在未处理异常时C++会调用terminate, 它会回调由std::set_terminate设置的处理函数,默认会调用abort。
最后来实现operator new,该函数的工作分为三个步骤:
- 调用std::set_new_handler,把Widget::current设置为全局的错误处理函数;
- 调用全局的operator new来分配真正的内存;
- 如果分配内存失败,Widget::current将会抛出异常;
- 不管成功与否,都卸载Widget::current,并安装调用Widget::operator new之前的全局错误处理函数。
我们通过RAII类来保证原有的全局错误处理函数能够恢复,让异常继续传播。关于RAII可以参见Item 13。 先来编写一个保持错误处理函数的RAII类:
class NewHandlerHolder{
public:
explicit NewHandlerHolder(std::new_handler nh): handler(nh){}
~NewHandlerHolder(){ std::set_new_handler(handler); }
private:
std::new_handler handler;
NewHandlerHolder(const HandlerHolder&); // 禁用拷贝构造函数
const NewHandlerHolder& operator=(const NewHandlerHolder&); // 禁用赋值运算符
};
Widget::operator new的实现
void * Widget::operator new(std::size_t size) throw(std::bad_alloc){
NewHandlerHolder h(std::set_new_handler(current));
return ::operator new(size); // 调用全局的new,抛出异常或者成功
} // 函数调用结束,原有错误处理函数恢复
客户使用Widget的方式也符合基本数据类型的惯例:
void outOfMem();
Widget::set_new_handler(outOfMem);
Widget *p1 = new Widget; // 如果失败,将会调用outOfMem
string *ps = new string; // 如果失败,将会调用全局的 new-handling function,当然如果没有的话就没有了
Widget::set_new_handler(0); // 把Widget的异常处理函数设为空
Widget *p2 = new Widget; // 如果失败,立即抛出异常
仔细观察上面的代码,很容易发现自定义”new-handler”的逻辑其实和Widget是无关的。我们可以把这些逻辑抽取出来作为一个模板基类:
template<typename T>
class NewHandlerSupport{
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void * operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler current;
};
template<typename T>
std::new_handler NewHandlerSupport<T>::current = 0;
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw(){
std::new_handler old = current;
current = p;
return old;
}
template<typename T>
void * NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc){
NewHandlerHolder h(std::set_new_handler(current));
return ::operator new(size);
}
有了这个class template,为widget添加set-new-handler支持能力就轻而易举了:
只要令widget继承自NewHandlerSupport<widget>
就好
class Widget: public NewHandlerSupport<Widget> {
...
//和先前一样但不必声明set_new_handler或operator new
};
operator new则应该抛出bad alloc异常
class widget{ ....};
widget* pw1 = new widget; //分配失败 抛出bad_alloc
if (pw1 == 0) ... //一定失败
widget* pw2 = new (std::nothrow) widget; //分配失败返回0
if (pw2 == 0) ... //可能成功
本文要点
- set-new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
- Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。
条款50: 了解new和delete的合理替换时机
替换编译器提供的operator new或operator delete理由:
- 检测使用错误。new得到的内存如果没有delete会导致内存泄露,而多次delete又会引发未定义行为。如果自定义operator new来保存动态内存的地址列表,在delete中判断内存是否完整,便可以识别使用错误,避免程序崩溃的同时还可以记录这些错误使用的日志。
- 提高效率。全局的new和delete被设计为通用目的(general purpose)的使用方式,通过提供自定义的new,我们可以手动维护更适合应用场景的存储策略。
- 收集使用信息。在继续自定义new之前,你可能需要先自定义一个new来收集地址分配信息,比如动态内存块大小是怎样分布的?分配和回收是先进先出FIFO还是后进先出LIFO?
- 实现非常规的行为。比如考虑到安全,operator new把新申请的内存全部初始化为0.
- 其他原因,比如抵消平台相关的字节对齐,将相关的对象放在一起等等。
定制型operator new
static const int signature = 0xDEADBEEF; // 边界符
typedef unsigned char Byte;
void* operator new(std::size_t size) throw(std::bad_alloc) {
// 多申请一些内存来存放占位符
size_t realSize = size + 2 * sizeof(int);
// 申请内存
void *pMem = malloc(realSize);
if (!pMem) throw bad_alloc();
// 写入边界符
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int)))
= *(static_cast<int*>(pMem)) = signature;
// 返回真正的内存区域
return static_cast<Byte*>(pMem) + sizeof(int);
}
这个operator new的缺点主要在于它疏忽了身为这个特殊函数所应该具备的“坚持C++规矩”的态度。
所有operator news都应该内含一个循环,反复调用某个new-handling函数。
齐位(alignment)意义重大,因为C++要求所有operator news返回的指针都有适当的对齐(取决于数据类型)。
本条款的主题是,了解何时可在“全局性的”或"class专属的”基础上合理替换缺省的new和delete:
- 为了检测运用错误
- 为了收集动态分配内存之使用统计信息
- 为了增加分配和归还的速度。
- 为了降低缺省内存管理器带来的空间额外开销。
- 为了弥补缺省分配器中的非最佳齐位(suboptimal alignment)。
- 为了将相关对象成簇集中。
- 为了获得非传统的行为。
本文要点
- 有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。
条款51: 编写new和delete时需固守常规
实现一致性operator new必得返回正确的值,内存不足时必得调用new-handling函数 ,必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new。
operator new的返回值十分单纯。如果它有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有那个能力,就遵循条款49描述的规则,并抛出一个badalloc异常。然而其实也不是非常单纯,因为operatornew实际上不只一次尝试分配内存,并在每次失败后调用new-handling函数。
奇怪的是C++规定,即使客户要求0bytes, operator new也得返回一个合法指针。
non-member operator new
void* operator new(std::size_t size) throw(std::bad_alloc) {
using namespace std; if (size ==0) {
size = 1;
}
while (true) {
尝试分配size bytes;
if (分配成功)
return (一个指针,指向分配得来的内存);
//分配失败;找出目前的new-handling函数(见下)
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler)
(*globalHandler)();
else
throw std::bad_alloc();
}
}
- size == 0时申请大小为1看起来不太合适,但它非常简单而且能正常工作。况且你不会经常申请大小为0的空间吧?
- 两次set_new_handler调用先把全局”new handler”设置为空再设置回来,这是因为无法直接获取”new handler”,多线程环境下这里一定需要锁。
- while(true)意味着这可能是一个死循环。所以Item 49提到,”new handler”要么释放更多内存、要么安装一个新的”new handler”,如果你实现了一个无用的”new handler”这里就是死循环了。
谈到operator new内含一个无穷循环,而上述伪码明白表明出这个循环: “while (true)“就是那个无穷循环。
退出此循环的唯一办法是:内存被成功分配或new-handling函数做了一件描述于条款49的事情:让更多内存可用、安装 另一个new-hander、卸除new-handler、抛出badalloc异常(或其派生物) ,或是承认失败而直接return。
重载operator new为成员函数通常是为了对某个特定的类进行动态内存管理的优化,而不是用来给它的子类用的。 因为在实现Base::operator new()时,是基于对象大小为sizeof(Base)来进行内存管理优化的。
当然,有些情况你写的Base::operator new是通用于整个class及其子类的,这时这一条规则不适用。
class Base{
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
};
class Derived: public Base{...};
Derived *p = new Derived; //调用了Base::operator new
子类继承Base::operator new()之后,因为当前对象不再是假设的大小,该方法不再适合管理当前对象的内存了。 处理此情势的最佳做法是将“内存申请量错误”的调用行为改采标准operator new
void *Base::operator new(std::size_t size) throw(std::bad_alloc){
if(size != sizeof(Base)) return ::operator new(size);
...
}
上面的代码没有检查size == 0!这是C++神奇的地方,大小为0的独立对象会被插入一个char(见Item 39)。 所以sizeof(Base)永远不会是0,所以size == 0的情况交给::operator new(size)去处理了。
这里提一下operator new[],它和operator new具有同样的参数和返回值, 要注意的是你不要假设其中有几个对象,以及每个对象的大小是多少,所以不要操作这些还不存在的对象。因为:
- 你不知道对象大小是什么。上面也提到了当继承发生时size不一定等于sizeof(Base)。
- size实参的值可能大于这些对象的大小之和。因为Item 16中提到,数组的大小可能也需要存储。
operatore delete
C++保证“删除null指针永远安全”
void operator delete(void *rawMem) throw(){
if(rawMem == 0) return;
// 释放内存
}
万一你的class专属的operator new将大小有误的分配行为转交::operator new执行,你也必须将大小有误的删除行为转交::operator delete执行:
class Base{
public:
static void * operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void *rawMem, std::size_t size) throw();
};
void Base::operator delete(void *rawMem, std::size_t size) throw(){
if(rawMem == 0) return; // 检查空指针
if(size != sizeof(Base)){
::operator delete(rawMem);
}
// 释放内存
}
如果即将被删除的对象派生自某个base class而后者欠缺virtual析 构函数,那么C++传给operatordelete的sizet数值可能不正确。
本文要点
- operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new_handler,它也应该有能力处理0 bytes申请。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。
- operator delete应该在收到null指针时不做任何事。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。
条款52: 写了placement new也要写placement delete
“placement new”通常是专指指定了位置的new(std::size_t size, void *mem),用于vector申请capacity剩余的可用内存。 但广义的”placement new”指的是拥有额外参数的operator new。
new和delete是要成对的,因为当构造函数抛出异常时用户无法得到对象指针,因而delete的责任在于C++运行时。 运行时需要找到匹配的delete并进行调用。因此当我们编写了”placement new”时,也应当编写对应的”placement delete”, 否则会引起内存泄露。在编写自定义new和delete时,还要避免不小心隐藏它们的正常版本。
class widget {
public:
//非正常形式的new
static void* operator new(std::size t size, std: :ostream& logStream) throw (std::bad alloc);
//正常的class专属delete
static void operator delete (void* pMemory std::size_t size) throw();
};
如果operatornew接受的参数除了一定会有的那个sizet之外还有其他,这 便是个所谓的placement new。因此,上述的operator new是个placement版本。众多placement new版本中特别有用的一个是“接受一个指针指向对象该被构造之处”:
void* operator new(std::size_t, void* pMemory) throw();
那个class将引起微妙的内存泄漏
widget* pw = new (std::cerr)widget;
如果内存分配成功,而widget构造函数抛出异常,运行期系统有责任取消operator new的分配并恢复旧观。然而运行期系统无法知道真正被调用的那个operator new如何运作,因此它无法取消分配并恢复旧观,所以上述做法行不通。
运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete。如果找到,那就是它的调用对象。
void operator delete(void*, std::ostream&) throw();
如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用。
class widget {
public:
static void* operator new(std::size t size, std::ostream& logstream) throw (std::bad alloc);
static void operator delete (void* pMemory) throw();
static void operator delete (void* pMemory, std::ostream& logstream) throw();
};
placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。对着一个指针(例如上述的pw)施行delete绝不会导致调用 placement delete, 这意味着避免内存泄漏我们需要一个正常的operatordelete(用于构造期间无任何异常被抛出)和一个placement版本(用于构造期间有异常被抛出)。后者的额外参数必须和operator new一样。
由于成员函数的名称会掩盖其外围作用域中的相同名称(见条款33),你必须小心避免让class专属的news掩盖客户期望的其他news (包括正常版 本) 。
class Base {
public:
static void* operator new(std::size t size, std::ostream& logStream)
throw(std::bad alloc);
};
Base* pb = new Base; //错误!因为正常形式的operator new被掩盖.
Base* pb = new (std::cerr) Base; //正确,调用Base的placement new.
为了避免全局的”new”被隐藏,先来了解一下C++提供的三种全局”new”:
void* operator new(std::size_t) throw(std::bad_alloc); //normal new
void* operator new(std::size_t, void*) throw(); //placement new
void* operator new(std::size_t, const std::nothrow_t&) throw(); //见Item 49
对于每一个可用的operator new也请确定提供对应的operator delete。如果你希望这些函数有着平常的行为,只要令你的class专属版本调用global版本即可。
class StandardNewDeleteForms {
public:
// normal new/delete
static void* operator new(std::size_t size) throw(std::bad_alloc) { return ::operator new(size); }
static void operator delete(void *pMemory) throw() { ::operator delete(pMemory); }
// placement new/delete
static void* operator new(std::size_t size, void *ptr) throw() { return ::operator new(size, ptr); }
static void operator delete(void *pMemory, void *ptr) throw() { return ::operator delete(pMemory, ptr); }
// nothrow new/delete
static void* operator new(std::size_t size, const std::nothrow_t& nt) throw() { return ::operator new(size, nt); }
static void operator delete(void *pMemory, const std::nothrow_t&) throw() { ::operator delete(pMemory); }
};
凡是想以自定形式扩充标准形式的客户,可利用继承机制及using声明式, 取得标准形式:
class widget: public StandardNewDeleteForms {
public:
using StandardNewDeleteForms::operator new;
using standardNewDeleteForms::operator delete;
static void* operator new(std::size t size std: :ostrean& logSt.ream) throw (std::bad alloc);
static void operator delete(void* pMemory, std::ostream& logstream) throw();
...
};
本文要点
- 当你写一个placement operator new,请确定也写出了对应的placementoperator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
- 当你声明placement new和placement delete,请确定不要无意识(非故意)地遮掩了它们的正常版本。
条款53: 不要轻忽编译器的警告
编译警告在C++中很重要,因为它可能是个错误啊! 不要随便忽略那些警告,因为编译器的作者比你更清楚那些代码在干什么。 所以,
- 请严肃对待所有warning,要追求最高warning级别的warning-free代码;
- 但不要依赖于warning,可能换个编译器有些warning就不在了。
一个常见错误
class B{
public:
virtual void f() const;
};
class D:public B{
public:
virtual void f();
};
这里希望以D::f重新定义virtual函数B::,但其中有个错误: B中的f是个const成员函数,而在D中它未被声明为const。我手上的一个编译器于是这样说 话了:
warning: D::f() hides virtual B::f()
这个编译器试图告诉你声明于B中的f并未在D中被重新声明, 而是被整个遮掩了。
本文要点
- 严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉。
- 不要过度倚赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本倚赖的警告信息有可能消失。
条款54: 让自己熟悉包括TR1在内的标准程序库
本文要点
- C++标准程序库的主要机能由STL, iostreams、 locales组成。并包含C99标准程序库。
- TR1添加了智能指针(例如trl::shared ptr)、一般化函数指针(tr1:: function) 、 hash-based容器、正则表达式(regular expressions)以及另外10个组件的支持。
- TR1自身只是一份规范。为获得TR1提供的好处,你需要一份实物。一个好的 实物来源是Boost
条款55: 让自己熟悉Boost
本文要点
- Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序库开发。Boost在CH+标准化过程中扮演深具影响力的角色。
- Boost提供许多TR1组件实现品,以及其他许多程序库。
Re: