条款 41 - 条款 52 | 《Effictive C++》笔记

Effictive C++ 条款 41 - 条款 52。

条款 41:了解隐式接口和编译期多态

  • 类和模板都支持接口和多态
  • 对类言接口是显式的,是通过函数签名实现的:函数签名描述了函数的参数和返回值;多态则是通过虚函数发生于运行期
  • 对模板而言接口是隐式的,是通过有效表达式实现的:使用模板的表达式规定了该模板具有什么成员函数(比如表达式 T.ShowName() 则表明了模板 T 具有名为 ShowName 的成员函数);多态则是通过模板具体化和函数重载解析发生于编译期

条款 42:了解 typename 的双重意义

  • 声明模板参数时,关键字 class 和 typename 意义完全相同
  • 在模板函数或者模板类中指涉一个嵌套从属类型名称时,就必须在其之前使用 typename 进行声明
1
2
3
template <typename C>              // 使用 typename 和 class 都行
void f(const C& container, // 不允许使用 typename
typename C::iterator iter); // 必须使用 typename

这是因为编译器不知道 iterator 是个类型还是个变量,比如表达式 C::iterator* x,如果 C::iterator 是 C 命名空间下的变量 iterator 时,* 就是一个乘号而非指针符号了。

  • 上述规则的例外是 typename 不可以出现在 base classes list 内的嵌套从属类型名称之前,也不可在成员初值列中作为基类的修饰符。例如:
1
2
3
4
5
6
7
8
template <typename T>
class Derived : public Base<T>::Nested { // base classes list 中不允许使用 typename
public:
explicit Derived(int x) : Base<T>::Nested(x) { // 成员初值列中不允许使用 typename
typename Base<T>::Nested temp; // 既不在 base classes list 中也不在成员初值列中
// 作为嵌套从属类型名称必须使用 typename
}
}

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

模板化的类作为基类时,有哪些要注意的地方。以一个例子说明,假设现在编写一个发送信息到不同公司的程序,信息要么译成密码,要么就是原始文字,在编译期间来决定哪一家公司发送至哪一家公司,采用模板方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class CompanyA {
public:
void sendCleartext(const std::string& msg);
void sendEncryted(const std::string& msg);
};

class CompanyB {
public:
void sendCleartext(const std::string& msg);
void sendEncryted(const std::string& msg);
};

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 <typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
void SendClearMsg(const MsgInfo& info) {
// 发送前的信息写到 log
sendClear(info); // 调用基类中的函数,这段代码无法通过编译
// 发送后的信息写到 log
}
};

上述代码编译不能通过,因为编译器看不到 sendClear 函数。因为当编译器遇到类模板 LoggingMsgSender 定义式时,不知道它继承什么样的类,因为 MsgSender 中的 Company 是个参数,在 LoggingMsgSender 被具体化之前,无法确切知道它是什么,自然而然就不知道类 MsgSender 是什么,也就不知道它是否有个 sendClear 函数。

为了让问题更具体化,假设现在有个 class CompanyZ 坚持使用加密通讯:

1
2
3
4
class CompanyZ {
pubic:
void sendEncryted(const std::sting& msg);
};

CompanyZ 没有 sendClear 函数,一般性的 MsgSender 模板对 CompanyZ 并不合适,这时我们可以针对 CompanyZ 产生一个 MsgSender 特化版:

1
2
3
4
5
6
7
template<>
class MsgSender<CompanyZ> {
public:
void sendSecret(const MsgInfo& infof) {
//
}
};

开头的 template<> 表示是特化版的 MsgSender 模板,在模板实参是 CompanyZ 时被使用。这就是模板全特化。接着回到刚才那个不能编译的问题上,如果 Company = CompanyZ,那么 sendClear 函数就不存在。编译器拒绝调用这个函数是因为它知道模板基类可能被特化,而那个特化版本可能不提供和一般性模板相同的接口。所以编译器拒绝在模板化基类中寻找继承而来的名称。

对此有三种解决方法:

  1. 在基类函数调用动作之前加上 this->
1
2
3
4
5
6
7
8
9
template <typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void SendClearMsg(const MsgInfo& info) {
// 发送前的信息写到 log
this->sendClear(info); // 告诉编译器,假设 sendClear 被继承
// 发送后的信息写到 log
}
};
  1. 使用 using 声明式:
1
2
3
4
5
6
7
8
9
10
template <typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
using MsgSender<Company>::sendClear; // 告诉编译器,假设 sendClear 位于基类内
void SendClearMsg(const MsgInfo& info) {
// 发送前的信息写到 log
sendClear(info); // 告诉编译器,假设 sendClear 将被继承
// 发送后的信息写到 log
}
};
  1. 明白指出被调用的函数位于基类内:
1
2
3
4
5
6
7
8
9
template <typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void SendClearMsg(const MsgInfo& info) {
// 发送前的信息写到 log
MsgSender<Company>::sendClear(info); // 告诉编译器,使用基类的 sendClear 函数
// 发送后的信息写到 log
}
};

第三种做法使用了明确资格修饰符(explicit qualification),这将会关闭 virtual 绑定行为。

这三种做法原理相同:对编译器承诺模板基类的任何特化版本都将支持其一般(泛化)版本所提供的接口。这样的承诺是编译器在解析派生类模板时需要的。但如果这个承诺稍后没有兑现,即使用 CompanyZ 作为 LoggingMsgSender 的模板参数,编译器还是会编译失败的。

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

在许多平台上 int 和 long 有相同的二进制表述,所以像 vector 和 vector<1ong> 的成员函数有可能完全相同。某些链接器会合并完全相同的函数实现码,但有些不会,后者意味某些模板被具现化为 int 和 1ong 两个版本,并因此造成代码膨胀。类似情况,在大多数平台上,所有指针类型都有相同的二进制表述,因此凡模板持有指针者(例如 1ist<int*>、1ist<const int*>、list<SquareMatrix<1ong,3>*> 等等)往往应该对每一个成员函数使用唯一一份底层实现。如果某些成员函数操作强型指针(即 T*),应该令它们调用另一个操作无类型指针(void*)的函数,由后者完成实际工作。某些 C++ 标准程序库的实现版本的确为 vector、deque 和 1ist 等模板类做了这件事。

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

模板具体化后的类不会因为具体化类型而存在派生关系。来看一个关于指针的例子,真实指针支持隐式转换,派生类指针可以隐式转换为基类指针,指向 non-const 对象的指针可以转换为指向 const 对象的指针,等等。例如:

1
2
3
4
5
6
class Top {};
class Middle : public Top };
class Bottom : public Middle{……};
Top* pt1 = new Middle; // Middle* 转换为 Top*
Top* pt2 = new Bottom; // Bottom* 转换为 Top*
const Top* pct2 = pt1; // Top* 转换为 const Top*

如果使用模板定义智能指针,上面的转换就有点麻烦了。SmartPtr 和 SmartPtr 之间的关系并不比 vector 和 Widget 之间的关系更密切,为了获得 SmartPtr 具体化后的类之间的转换能力,必须将它们明确地编写出来。但是这是永远无法做到的,如果这个继承体系未来有所扩充,那就需要再次修改 SmartPtr。

所以 SmartPtr 需要的不是一个构造函数,而是一个构造模板。这样的模板就是所谓的 member function template,其作用是为类生成函数:

1
2
3
4
5
6
template <typename T>
class SmartPrt {
public:
template <typename U> // member function template
SmartPtr(const SmartPrt<U>& other); // 为了生成拷贝构造函数
};

以上代码意思是,对任何类型 T 和任何类型 U,可以根据 SmartPrt 生成一个 SmartPtr,因为 SmartPtr 有个构造函数接受一个 SmartPrt 参数,这就是泛化拷贝构造函数。该函数没有声明为 explicit 是因为转换可能是隐式的。

上述代码并不完整,我们希望根据一个 SmartPtr 创建 SmartPtr,却不希望根据一个 SmartPtr 创建一个 SmartPtr。我们可以在“构造模板”实现代码中约束转换行为:

1
2
3
4
5
6
7
8
9
10
11
template <typaname T>
class SmartPtr {
public:
template <typename U>
SmartPrt(const SmartPrt<U>& other) : heldPrt(other.get()) {};
T* get() const {
return heldPrt;
}
private:
T* heldPrt;
};

在上述代码中,存在一个将将 U* 转换为 T* 的隐式转换,这约束了转换行为。

在类内声明泛化拷贝构造函数并不阻止编译器生成它们自己的拷贝构造函数(non-template)。如果想要控制拷贝构造函数的方方面面,就要声明正常的拷贝构造函数。相同的规则也适用于赋值操作。

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

条款 24 提到过为什么 non-member 函数才有能力“在所有实参身上实施隐式类型转换”,本条款是条款 24 的扩展,将 Rational 变成模板类:

1
2
3
4
5
6
7
8
9
10
template <typename T>
class Rational {
public:
Rational(const T& numerator, const T& denominator);
};

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 参数那个operator*),但是这里编译器不知道。

本例中类型参数分别是 Rational 和 int。operator* 的第一个参数被声明为 Rational,传递给operator* 的第一个实参 oneHalf 的类型正是 Rational,所以 T 一定是 int。operator* 的第二个参数类型被声明为 Rational,但传递给 operator* 的第二个实参类型是int,编译器如何推算出 T?或许你期望编译器使用 Rational 的 non-explicit 构造函数将 2 转换为 Rational,进而推导出 T 为 int,但它不这么做,因为在模板实参推导过程中从不将隐式类型转换考虑在内

模板函数在调用的时候(具体化后)会出现隐式转换(运行期),但是在推导模板参数的时候不会发生隐式转换(编译期)。

模板类内的友元函数可以解决这个问题:

1
2
3
4
5
6
7
8
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) {};

这时候对 operator* 的混合调用可以通过编译了。oneHalf 被声明时,Rational 类被具体化为 Rational,而作为过程的一部分,其友元函数也就自动具体化出来。因此编译器在调用它的时候就可以使用隐式转换(将 int 转换为 Rational),所以混合调用可以通过编译。在一个模板类内,模板名称可被用来作为模板及其参数的简略表达方式,所以在 Rational 内,可以把 Rational 简写为 Rational。

虽然通过编译,但是这段代码却无法链接,因为我们只在类内声明了这个函数,却在类外进行定义,链接器找不到的对应的实现。一个最简单的办法就是将该函数的定义合并到其声明内。

这个技术虽然使用了友元,却与传统的友元用途“访问类的 non-public 成员”不同。为了让类型转换可能发生与所有实参身上,我们需要一个 non-member 函数(条款 24);为了让这个函数被自动具体化,我们需要将其声明在类内;而在类内声明 non-member 函数的唯一办法就是让它成为一个友元。

条款 47:请使用 traits classes 表现类型信息

Traits 并不是关键字或者一个预先定义好的构件,这是一种技巧,也是 C++ 程序员共同遵守的协议。这个技术的要求之一是:对内置的类型和用户自定义的类型的表现必须一样好。这种技巧可以允许我们在编译期间取得某些类型信息。习惯上 traits 总是被实现为 struct,所以它们往往被称为 traits classes。

假如我们要自己实现 STL 容器中的 advance 函数,对于不同的容器类型,需要不同的实现,比如 list 的迭代器只能步进,而 vector 则可以任意访问,所以需要有一个类来表明迭代器的分类。迭代器 traits classes 在这里起到的作用就是萃取出迭代器的内容类型。

这就要求在容器的定义中必须有关于迭代器类型的声明。例如 vector 的迭代器可以随机访问,其 class 看起来是这样的:

1
2
3
4
5
6
7
8
9
10
template < ... >
class vector {
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};

list 的迭代器可以双向步进,所以它应该是这样的:

1
2
3
4
5
6
7
8
9
10
template < ... >
class vector {
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};

所以迭代器 traits classes iterator_traits 的实现就可以是这样的:

1
2
3
4
5
template <typename IterT>
class iterator_traits {
typedef typename IterT::iterator_category iterator_category;
...
}

但是这对指针(也是一种迭代器)行不通,因为指针内部不可能有 typedef 的定义,所以需要有一个 iterator_traits 的偏特化模板专门针对指针:

1
2
3
4
5
template <typename IterT>
class iterator_traits<IterT*> {
typedef random_access_iterator_tag iterator_category;
...
}

现在的 iterator_traits 可以用于 advance 的实现了:

1
2
3
4
5
6
7
template <typename IterT, typename DistT>
class advance<IterT& iter, DistT d> {
if (typeid(typename std::iterator_traits<IterT>::iterator_category) == typeid(std::random_access_iterator_tag)) {
...
}
...
}

但是这里有一个问题,IterT 类型在编译期获知,所以 std::iterator_traits::iterator_category 也可在编译期确定。但 if 语句却是在运行期才会确定,没必要将可在编译期完成的事延到运行期才做,这不仅浪费时间,也还造成可执行文件膨胀。函数重载就是在编译期进行类型条件判断的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 原函数
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
doAdvance(iter, d, 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");
}
while (d--) ++iter;
}

条款 48:认识 template 元编程

模板元编程有三个好处:

  1. 它让某些事情更容易,如果没有它,有些事情将是困难的,甚至不可能的。
  2. 由于模板元编程执行于编译期,因此可将工作从运行期转移到编译期。这导致的一个结果是:某些错误原本通常在运行期才能侦测到,现在可在编译期找出来。
  3. 另一个结果是,使用模板元编程的程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期移转至编译期的另一个结果是编译时间变长了。

条款 49:了解 new-handler 的行为

什么是 new-handler

当 operator new 执行失败时(如没有足够的内存),它会抛出异常(在某些旧编译器上会返回空指针)。而在抛出异常前,它会先调用一个客户指定的错误处理函数,一个所谓的 new-handler,这个函数可以由 set_new_handler 函数指定。

1
2
3
4
namespace std {
typedef void(*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}

new_handler 是个函数指针,该函数没有参数也不返回任何东西。set_new_handler 是获得一个 new_handler 并返回一个 new_handler 的函数,返回的 new_handler 是指向 set_new_handler 被调用前正在执行的那个 new-handler 函数。后面的 throw() 是一份异常明细,表示该函数不抛出异常。

当 operator new 无法满足内存申请时,它会不断调用 new-handler,直到找到足够内存。设计良好的 new-handler 必须做好以下事情:

  • 让更多内存可被使用。这样可以造成 operator new 内的下一次内存分配动作可能成功。一个做法是,程序一开始就分配一大块内存,当 new-handler 第一次被调用时将它们给程序使用
  • 安装另一个 new-handler:当前的 new-handler 无法取得更多内存时,安装另一个 new-handler 以替换自己
  • 卸载 new-handler:即将 null 指针传给 set_new_handler,一旦没有安装任何 new-handler,operator new 在内存分配不成功时便抛出异常
  • 抛出 bad_alloc(或派生自 bad_alloc)的异常:这样的异常不会被 operator new 捕捉,因此会在调用 operator new 之处被捕捉
  • 不返回:通常 abort 或 exit

如何为类定制专属 new-handler

C++ 并不支持类专属的 new-handler,但是我们自己可以实现这种行为。令每一个类提供自己的 set_new_handler 和 operator new 即可。现在打算处理 Widget 类内存分配失败的情况,首先要有一个当 operator new 无法为 Widget 分配足够内存时的调用函数,即 new_handler 函数。

Widget 的客户应该这样设置其 new-handler:

1
2
3
4
5
6
void outOfMem();
Widget::set_new_handler(outOfMem); // 设定 outOfMem 为 Widget 的 new-handler 函数
Widget* pw1 = new Widget; // 内存分配失败 则调用 OutOfMem
std::string* ps = new std::string; // 内存分配失败则调用 global new-handler(如果有)
Widget::set_new_handler(0); // 设定 Widget 专属 new-handler 为 null
Widget* pw2 = new Widget; // 内存分配失败则立刻抛出异常

Widget 的 set_new_handler 函数是的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 currentHandler;
};

// static 成员必须在类定义式之外被定义(除非它们是 const 整型,不单指 int,还包括 long、char 等)
std::new_handler Widget::currentHandler = 0;
// Widget 内的 set_new_handler 函数会将它获得的指针存储起来,然后返回先前(在此调用之前)存储的指针,这也是标准版 set_new_handler 的行为(在此调用之前)存储的指针,这也正是标准版 set_new_handler 的作为:
std::new_handler Widget::set_new_handler(std::new_handler p) throw() {
std::new_handler oldHandler = currentHandler;
currentHandler = p;
reutrn oldHandler;
}

Widget 的 operator new 会做以下事情:

  1. 调用标准 set_new_handler,告知 Widget 错误处理函数。这会将 Widget 的 new-handler 安装为 Widget 的 new-handler
  2. 调用 global operator new,如果失败,global operator new 会调用 Widget 的 new-handler,因为在上一步中 Widget 的 new-handler 被安装为 global new-handler。如果 global operator new 最终无法分配足够内存,会抛出一个 bad_alloc 异常。这时 Widget 的 operator new 要恢复原本的 global new-handler,之后再传播异常
  3. 如果 global operator new 调用成功,Widget 的 operator new 会返回一个指针,指向分配的内存。Widget 析构函数会管理 global new-handler,它会将 Widget 的 operator new 被调用前的那个 global new-handler 恢复回来

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {} // 取得目前的 new_handler
~NewHandlerHolder() { std::set_new_handler(handler); } // 释放它
private:
std::new_handler handler;
NewHandlerHolder& (const NewHandlerHolder&); // 阻止 copy
NewHandlerHolder& operator=(const NewHandlerHolder&); // 阻止 asign
};

void* Widget::operator new(std::size_t size) throw(std::bad_alloc) {
NewHandlerHolder h(std::set_new_handler(currentHandler)); // 这时 currentHandler 是 outOfMem
return ::operator new(size);
}
  • 类的专属 set_new_handler 用以保存旧的 new-hanlder 以供恢复和新的 new-handler 以供在 new 的时候进行设置
  • 类在 new 时调用标准 set_new_handler 将 new-hanlder 设置为类专属 new-handler,使用标准 operator new 分配内存,然后将 new-hanlder 恢复成旧的

如何使用 CRTP 为类定制专属 new-handler

实现这个方案的 class 代码基本相同,用个基类加以复用是个好的方法。可以用个 template 基类,如此以来每个派生类将获得实体互异的 class data 复件。这个基类让其派生类继承它的 set_new_handler 和 operator new,template 部分确保每一个派生类获得一个实体互异的 currentHandler 成员变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 currentHandler;
};

template <typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw() {
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}

template <typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc) {
NewHandlerHolder h(std::set_new_handler(currentHandler);
return ::operator new(size);
}

// 将每一个 currentHandler 初始化为 null
template <typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;

// 有了这个 class template,为 Widget 添加 set_new_handler 就容易了
class Widget : public NewHandlerSupport<Widget> {
...
};

在 template 基类中,从未使用类型 T。因为 currentHandler 是 static 类型,使用模板是为了保证每个 class 都有自己的 currentHandler。

最后

Nothrow new 对异常的强制保证性并不高。new(std::nothrow) Widget 发生两件事,第一分配内存给 Widget 对象,如果失败返回 null 指针。第二,如果成功,调用Widget 的构造函数,但是在这个构造函数做什么,nothrow new 并不知情。如果在构造函数使用 operator new 分配内存,那么还是有可能抛出异常并传播。使用 nothrow new 只能保证 operator new 不抛出异常,不能保证像 new(std::nothrow) Widget 这样的表达式不抛出异常。所以,并没有运用 nothrow 的需要。

条款 50:了解 new 和 delete 的合理替换时机

替换 new 和 delete 的原因:

  • 检测运用上的错误:在分配内存的前后两端放置签名以检测是否会错误写入内存
  • 强化效能:标准的 operator new 是为了所有场景设计的,在某些特定场景上的性能可能达不到最优,针对自己的使用场景进行定制可以获得最优的性能
  • 收集使用上的统计数据
  • 为了增加分配和归还的速度(内存池):使用定制的针对特定类型对象的分配器,可以提高效率,例如 Boost 库的 Pool
  • 为了降低缺省内存管理器带来的空间额外开销:泛用型分配器往往(虽然并非总是)不只比定制型慢,还使用更多空间,因为它们常常在每一个分配区块上招引某些额外开销。针对小型对象开放的分配器,例如 Boost 库的 Pool,本质上消除了这样的额外开销
  • 为了弥补缺省分配器的非最佳对齐(suboptimal alignment):x86 体系结构上的 double 在 8-byte 对齐的情况下访问速度最快,但是编译器自带的 operator new 并不保证分配 double 是 8-byte 对齐
  • 为了将相关对象成簇集中:如果特定的某个数据结构往往被一起使用,我们希望在处理这些数据时将缺页中断的频率降至最低,那么为此数据结构创建另一个 heap 就有意义,这样就可以将它们成簇集中到尽可能少的内存页上
  • 为了获得非传统的行为:有时候我们需要做 operator new 和 operator delete 没做的事,例如,归还内存时将其数据覆盖为 0,以此增加应用程序的数据安全

条款 51:编写 new 和 delete 时需固守常规

operator new

下面是个 non-member operator new 的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void* operator new(std::size_t size) throw(std::bad_alloc) {
if (size == 0) { // 处理 0-byte 申请
size = 1;
}
while(true) {
尝试分配 size bytes;
if (分配成功)
return 指向分配得来的内存;
// 分配失败,找到当前的 new-handler 函数,这么做是因为没有任何办法可以直接取得 new-hander 函数指针
std::new_handler globalHandler = std::set_new_handler(0);
std::set_new_handler(globalHandler);

if (globalHandler)
(*globalHandler)();
else
throw std::bad_alloc();
}
}

上面包含一个死循环,退出此循环唯一办法就是内存被成功分配或 new-handler 函数做了一件描述于条款 49 的事情:

  • 让更多内存可用
  • 安装另一个 new-handler
  • 卸除 new-handler
  • 抛出 bad_alloc 异常(或其派生物)
  • 或是承认失败而直接 return

上面的 operator new 成员函数可能会被 derived classes 继承,定制内存分配器往往是为了特定的类,以此来优化,并不是针对该类的派生类,所以应该像这样:

1
2
3
4
void* Base::operator new(std::size_t size) throw(std::bad_alloc) {
if (size != sizeof(Base))
return ::operator new(size); // 使用标准的 operator new
}

operator delete

operator delete 情况就简单很多,但是要记住,C++ 保证删除空指针永远安全。这个函数的 member 版本也很简单,只需多加一个检查删除数量:

1
2
3
4
5
6
7
8
9
void Base::operator delete(void rawMemory, std::size_t size) throw() {
if (rawMemory == 0) return;
if (size != sizeof(Base)) {
::operator delete(rawMemory);
return ;
}
归还 rawMemory 所指内存;
return ;
}

总结

编写 operator new 和 operator delete 的

  • operator new
    • 死循环
    • 针对派生类的行为
  • operator delete
    • 删除空指针安全
    • 针对派生类的行为

条款 52:写了 placement new 也要写 placement delete

Operator new 会做两件事情:分配内存和构造对象。如果内存已经提前分配好了,直接在分配好的内存地址上构造对象是比较合理的需求。

如果 operator new 接受的参数除了一定会有的那个 size_t 之外还有其他,这便是所谓的 placement new。众多 placement new 版本中特别有用的一个是“接受一个时针指向对象该被构造之处”,其形式如下:

1
void* operator new(std::size_t, void* pMemory) throw();

这个版本的 operator new 已被纳入 C++ 标准程序库,#include 便可以使用。这个 operator new 的用途之一是负责在 vector 的未使用空间上创建对象。这个 placement new 是最早的版本,根据命名:一个特定位置上的 new。当人们谈到 placement new 时,大多时候谈的是这一特定版本,即还有额外实参 void*。

在使用 operator new 创建对象时,有两个函数被调用,第一个函数就是 operator new,用以分配内存,第二个是类的构造函数。如果第一个函数调用成功,但是第二个函数调用失败,这时需要释放第一步分配的内存,否则就造成了内存泄露。这个时候,如果使用的是 placement new 且没有对应的 placement delete,那么之前分配的内存就无法释放,造成内存泄漏。

在默认情况下,C++ 在 global 作用域内提供以下形式的 operator new:

1
2
3
void* operator(std::size_t) throw(std::bad_alloc);
void* operator(std::size_t, void*) throw();
void* operator(std::size_t, const std::nothrow_t&) throw();

在类内声明任何形式的 operator new 都会隐藏上面这些标准形式,为避免这种隐藏,一个简单的做法是建立一个基类,内含所有正常形式的 operator new 和 delete,形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class StadardNewDeleteForms{
public:
// normal
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
static void* operator new(std::size_t size, void* ptr) throw(std::bad_alloc) {
return ::operator new(size, ptr);
}
static void operator delete(void* pMemory, void* ptr) throw() {
::operator delete(pMemory, ptr);
}

// nothrow
static void* operator new(std::size_t size, const std::nothrow_t& nt) throw(std::bad_alloc) {
return ::operator new(size,nt);
}
static void operator delete(void* pMemory,const std::nothrow_t&) throw() {
::operator delete(pMemory);
}
};

如果想以自定义方式扩充标准形式,可以使用继承机制和 using 声明:

1
2
3
4
5
6
7
8
9
class Widget : public StandardNewDeleteForms {
public:
// 让这些形式可见
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
// 添加自己定义的
static void* operator new(std::size_t size, std::ostream& logStream) throw(std:;bad_alloc);
static void operator detele(std::size_t size, std::ostream& logStream) throw();
};

条款 41 - 条款 52 | 《Effictive C++》笔记

http://www.zh0ngtian.tech/posts/bc7a3a0f.html

作者

zhongtian

发布于

2020-08-30

更新于

2023-12-16

许可协议

评论