• 读书笔记 effective c++ Item 52 如果你实现了placement new,你也要实现placement delete


    1. 调用普通版本的operator new抛出异常会发生什么?

    Placement new和placement delete不是C++动物园中最常遇到的猛兽,所以你不用担心你对它们不熟悉。当你像下面这样实现一个new表达式的时候,回忆一下Item 16Item 17

    1 Widget *pw = new Widget;

    两个函数会被调用:一个是调用operator new来分配内存,第二个是Widget的默认构造函数。

    假设第一个调用成功了,但是调用第二个函数抛出了异常。在这种情况下,对步骤一中执行的内存分配必须进行回滚。否则就会发生内存泄漏。客户端代码不能释放内存,因为如果Widget构造函数抛出了异常,pw永远不会赋值。客户端就没有办法得到指向需要释放内存的指针。对步骤一进行回滚的责任就落在了C++运行时系统身上。

    运行时系统很高兴去调用与步骤1中调用的operator new版本相对应的operator delete,但是只有在它知道哪个operator delete(可能有许多)是合适的被调用函数的情况下才能做到。如果你正在处理的new和delete版本有着正常的签名,那么这不是一个问题,因为正常的operator new,

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

    对应着正常的operator delete:

    1 void operator delete(void *rawMemory) throw();     // normal signature
    2 // at global scope
    3 
    4 void operator delete(void *rawMemory, std::size_t size) throw();   // typical normal signature at class  scope        

    2. 调用自定义operator new抛出异常会发生什么?

    2.1 一个有问题的例子

    如果你正在使用普通形式的new和delete,运行时系统能够找到new对应版本的delete来执行回滚操作。然而,如果你开始声明非普通版本的new——也就是生成一个带参数的版本,“哪个delete才是new对应的版本”这个问题就出现了。

    例如,假设你实现了一个类特定版本的operator new,它需要指定一个ostream来为内存分配信息进行记录,你同样实现了一个普通的类特定版本的operator delete:

     1 class Widget {
     2 public:
     3 ...
     4 
     5 static void* operator new(std::size_t size,
     6 
     7                                                          // non-normal
     8 std::ostream& logStream)                    // form of new
     9 throw(std::bad_alloc);
    10 static void operator delete(void *pMemory, // normal class
    11 std::size_t size) throw(); // specific form
    12 // of delete
    13 ...
    14 };

    2.2 对相关术语的说明

    这个设计是有问题的,但是在我们讨论原因之前,我们需要对相关术语进行说明。

    当一个operator new函数带了额外的参数(除了必须要带的size_t参数)的时候,我们知道这是new的placement版本。上面的operator new就是这样一个placement版本。一个尤为有用的placement new是带有一个指针参数,指定对象应该在哪里被构建。它会像下面这个样子:

    1 void* operator new(std::size_t, void *pMemory) throw(); // “placement
    2 // new”

    这个版本的new是C++标准库的一部分,只要你#inlucde <new>就能够访问它。它也用来在vector的未被使用的空间中创建对象。它还是最早的placement new。事实上,这也是这个函数的命名依据:在特定位置上的new。这就意味着“placement new”被重载了。大多情况下当人们谈到placement new的时候,它们讨论的是这个特定的函数,也即是带有一个void *额外参数的operator new。少数情况下,它们讨论的是带有额外参数的任意版本的operator new。程序的上下文往往会清除这种模棱两可,但是明白普通术语“placement new”意味着带额外参数的任意new版本是很重要的事,因为“placement delete”(我们一会会碰到)直接派生自它。

    2.3 如何解决问题

    现在让我们回到对Widget 类的声明上来,我在前面说过这个设计是有问题的。难点在于这个类会发生微妙的内存泄漏。考虑下面的客户代码,在动态创建一个Widget的时候它将内存分配信息记录到cerr中:

    1 Widget *pw = new (std::cerr) Widget; // call operator new, passing cerr as
    2 // the ostream; this leaks memory
    3 // if the Widget constructor throws

    还是上次的问题,当内存分配成功了,但是Widget构造函数抛出了异常,运行时系统有责任将operator new执行的分配工作进行回滚。然而,运行时系统不能够真正明白被调用的operator new版本是如何工作的,所以它不能够自己进行回滚操作。相反,运行时系统会寻找一个operator delete,它和operator new带有相同数量和类型的额外参数,如果找到了,那么这个就是它要调用的版本。在上面的例子中,operator new带有一个额外的参数ostream&,所以对应的operator delete就是:

    1 void operator delete(void*, std::ostream&) throw();

    同new的placement 版本进行对比,带有额外参数的operator delete版本被叫做placement delete。在这种情况下,Widget没有声明operator delete的placement 版本,所以运行时系统不知道如何对placement new的操作进行回滚。因此它不会做任何事情。在这个例子中,如果Widget构造函数抛出异常之后没有operator delete会被调用!

    规则很简单:如果一个带了额外的参数operator new 没有与之相匹配的带有相同额外参数的operator delete版本,如果new的内存分配操作需要被回滚那么没有operator delete会被调用。为了消除上面代码的内存泄漏,Widget需要声明一个与记录日志的placement new版本相对应的placement delete:

     1 class Widget {
     2 public:
     3 ...            
     4 
     5 static void* operator new(std::size_t size, std::ostream& logStream)
     6 throw(std::bad_alloc);
     7 static void operator delete(void *pMemory) throw();
     8 static void operator delete(void *pMemory, std::ostream& logStream)
     9 throw();
    10 ...
    11 };

    有了这个改动,在下面的语句中,如果异常从Widget构造函数中抛出来:

    1 Widget *pw = new (std::cerr) Widget; // as before, but no leak this time

    对应的placement delete会被自动被调用,这就让Widget确保没有内存被泄漏。

    3. 调用delete会发生什么?

    然而,考虑如果没有异常被抛出的时候会发生什么,我们会在客户端代码中进行delete:

    1 delete pw; // invokes the normal
    2 // operator delete

    正如注释所说明的,这会调用普通的operator delete,而不是placement 版本。Placement delete只有在构造函数中调用与之相匹配的placement new时抛出异常的时候才会被触发。对一个指针使用delete(就像上面的pw一样)永远不会调用delete的placement版本。

    这就意味着为了对new的placement 版本造成的内存泄漏问题进行先发制人,你必须同时提供operator delete的普通版本(在构造期间没有异常抛出的时候调用),以及和placement new带有相同额外参数的placement版本(抛出异常时调用)。做到这一点,在内存泄漏的微妙问题上你就永远不需要在辗转反侧难以入睡了。

    4. 注意名字隐藏问题

    顺便说一下,因为成员函数名字会隐藏外围作用域中的相同的名字(见Item 33),你需要小心避免类特定的new版本把客户需要的其他版本隐藏掉(包括普通版本)。例如如果你有一个基类只声明了一个operator new的placement 版本,客户将会发现它们不能再使用new的普通版本了:

     1 class Base {
     2 public:
     3 ...
     4 static void* operator new(std::size_t size, // this new hides
     5 std::ostream& logStream) // the normal
     6 throw(std::bad_alloc); // global forms
     7 ...
     8 };
     9 
    10 Base *pb = new Base;                  // error! the normal form of
    11 // operator new is hidden
    12 
    13 Base *pb = new (std::cerr) Base; // fine, calls Base’s
    14 // placement new

    类似的,派生类中的operator new会同时把operator new的全局版本和继承版本隐藏掉:

     1 class Derived: public Base {         // inherits from Base above
     2 
     3 
     4 public:
     5 ...
     6 static void* operator new(std::size_t size) // redeclares the normal
     7 throw(std::bad_alloc); // form of new
     8 ...
     9 };
    10 Derived *pd = new (std::clog) Derived; // error! Base’s placement
    11 // new is hidden
    12 Derived *pd = new Derived; // fine, calls Derived’s
    13 // operator new

    Item 33中非常详细的讨论了这种类型的名字隐藏,但是为了实现内存分配函数,你需要记住的是默认情况下,C++在全局范围内提供了如下版本的operator new:

    1 void* operator new(std::size_t) throw(std::bad_alloc);          // normal new
    2 
    3 void* operator new(std::size_t, void*) throw();    // placement new
    4 
    5 void* operator new(std::size_t,                             // nothrow new —
    6 const std::nothrow_t&) throw(); // see Item 49

    如果你在类中声明了任何operator new,你就会隐藏这些标准版本。除非你的意图是防止客户使用这些版本,否则除了任何你所创建的自定义operator new版本之外,确保这些标准版本能够被客户所用。对每个你所提供的operator new,确保同时提供相对应的operator delete。如果你想让这些函数的行为同普通函数一样,让你的类特定版本调用全局版本就可以了。

    实现这个目的的一种简单的方法是创建一个包含所有new 和delete版本的基类:

     1 class StandardNewDeleteForms {
     2 public:
     3 // normal new/delete
     4 static void* operator new(std::size_t size) throw(std::bad_alloc)
     5 { return ::operator new(size); }
     6 static void operator delete(void *pMemory) throw()
     7 { ::operator delete(pMemory); }
     8 
     9 // placement new/delete
    10 static void* operator new(std::size_t size, void *ptr) throw()
    11 { return ::operator new(size, ptr); }
    12 static void operator delete(void *pMemory, void *ptr) throw()
    13 { return ::operator delete(pMemory, ptr); }
    14 // nothrow new/delete
    15 static void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
    16 { return ::operator new(size, nt); }
    17 static void operator delete(void *pMemory, const std::nothrow_t&) throw()
    18 { ::operator delete(pMemory); }
    19 };

    客户如果想在自定义版本的基础上增加标准版本,只需要继承这个基类然后使用using声明就可以(Item 33)获得标准版本:

     1 class Widget: public StandardNewDeleteForms {       // inherit std forms
     2 
     3 public:                                                               
     4 
     5 
     6 using StandardNewDeleteForms::operator new; // make those
     7 
     8 using StandardNewDeleteForms::operator delete;    // forms visible
     9 
    10 static void* operator new(std::size_t size,        // add a custom
    11 
    12 
    13 std::ostream& logStream) // placement new
    14 throw(std::bad_alloc);
    15 static void operator delete(void *pMemory, // add the corres
    16 std::ostream& logStream) // ponding place
    17 throw(); // ment delete
    18 ...
    19 };

    5. 总结

    • 当你实现operator new的placement版本的时候,确保实现与之相对应的operator delete placement版本。如果你不进行实现,有的程序会发生微妙的,间歇性的内存泄漏。
    • 当你声明new和delete的placement版本的时候,确保不要无意间隐藏这些函数的普通版本。
  • 相关阅读:
    欧拉回路 定理
    UESTC 1087 【二分查找】
    POJ 3159 【朴素的差分约束】
    ZOJ 1232 【灵活运用FLOYD】 【图DP】
    POJ 3013 【需要一点点思维...】【乘法分配率】
    POJ 2502 【思维是朴素的最短路 卡输入和建图】
    POJ 2240 【这题貌似可以直接FLOYD 屌丝用SPFA通过枚举找正权值环 顺便学了下map】
    POJ 1860【求解是否存在权值为正的环 屌丝做的第一道权值需要计算的题 想喊一声SPFA万岁】
    POJ 1797 【一种叫做最大生成树的很有趣的贪心】【也可以用dij的变形思想~】
    js 实现slider封装
  • 原文地址:https://www.cnblogs.com/harlanc/p/6747528.html
Copyright © 2020-2023  润新知