我应该如何编写符合 ISO C++ 标准的自定义 new 和 delete 运算符?
这是极具启发性的 C++ 常见问题解答 Operator overloading 及其后续内容 Why should one replace default new and delete operators? 中 Overloading new and delete 的延续
第 1 部分:编写符合标准的 new
运算符
第 1 部分:了解编写自定义 new 运算符的要求
第 2 部分:了解 new_handler 要求
第 3 部分:了解特定场景要求
第 2 部分:编写符合标准的 delete
运算符
-
Implementing Custom delete operator
_(注意:这是 [Stack Overflow 的 C++ 常见问题解答](https://stackoverflow.com/questions/tagged/c++-faq) 的一个条目。如果您想批评以这种形式提供常见问题解答的想法,然后[开始这一切的 meta 上的帖子](https://meta.stackexchange.com/questions/68647/setting-up-a-faq-for-the-c-tag)将是这样做的地方. 该问题的答案在 [C++ 聊天室](https://chat.stackoverflow.com/rooms/10/c-lounge) 中进行监控,FAQ 想法最初就是从那里开始的,所以你的答案很可能让提出这个想法的人阅读。)_ *注意:答案基于 Scott Meyers 的更有效 C++ 和 ISO C++ 标准的学习。*
c++-faq
并不适用于普通用户能想到的每一个自答书式问答。
第一部分
This C++ FAQ entry 解释了为什么可能希望为自己的类重载 new
和 delete
运算符。本常见问题解答试图以符合标准的方式解释如何。
实现自定义新运算符
C++ 标准(§18.4.1.1)将 operator new
定义为:
void* operator new (std::size_t size) throw (std::bad_alloc);
C++ 标准在 §3.7.3 和 §18.4.1 中指定了这些运算符的自定义版本必须遵守的语义
让我们总结一下要求。
要求 #1: 它应该动态分配至少 size
个字节的内存并返回一个指向已分配内存的指针。引用 C++ 标准,第 3.7.4.1.3 节:
分配函数尝试分配请求的存储量。如果成功,它将返回存储块的开始地址,其字节长度应至少与请求的大小一样大......
该标准进一步规定:
...返回的指针应适当对齐,以便它可以转换为任何完整对象类型的指针,然后用于访问已分配存储中的对象或数组(直到通过调用相应的存储显式释放存储)释放函数)。即使请求的空间大小为零,请求也可能失败。如果请求成功,则返回的值应是与任何先前返回的值 p1 不同的非空指针值 (4.10) p0,除非该值 p1 随后被传递给操作员删除。
这给了我们进一步的重要要求:
要求 #2: 我们使用的内存分配函数(通常是 malloc()
或其他一些自定义分配器)应该返回一个 适当对齐 指向已分配内存的指针,该指针可以转换指向完整对象类型的指针,用于访问该对象。
要求 #3: 即使请求零字节,我们的自定义运算符 new
也必须返回合法指针。
甚至可以从 new
原型推断出的明显要求之一是:
要求 #4: 如果 new
无法分配所请求大小的动态内存,那么它应该抛出 std::bad_alloc
类型的异常。
但是! 这比表面上看到的要多:如果您仔细查看 new
运算符 documentation(来自标准的引文如下),它指出:
如果已经使用 set_new_handler 定义了一个 new_handler 函数,那么这个 new_handler 函数在它不能自己分配所请求的存储空间时由 operator new 的标准默认定义调用。
要了解我们的自定义 new
需要如何支持此要求,我们应该了解:
new_handler 和 set_new_handler 是什么?
new_handler
是指向函数的指针的 typedef,它不接受和不返回任何内容,而 set_new_handler
是接受并返回 new_handler
的函数。
set_new_handler
的参数是一个指针,指向函数 operator new 如果它不能分配所请求的内存就应该调用。它的返回值是指向先前注册的处理函数的指针,如果没有先前的处理函数,则返回 null。
一个代码示例让事情变得清晰的好时机:
#include <iostream>
#include <cstdlib>
// function to call if operator new can't allocate enough memory or error arises
void outOfMemHandler()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
//set the new_handler
std::set_new_handler(outOfMemHandler);
//Request huge memory size, that will cause ::operator new to fail
int *pBigDataArray = new int[100000000L];
return 0;
}
在上面的示例中,operator new
(很可能)将无法为 100,000,000 个整数分配空间,函数 outOfMemHandler()
将被调用,程序将在 issuing an error message 之后中止。
需要注意的是,当 operator new
无法满足内存请求时,它会重复调用 new-handler
函数,直到它找到足够的内存或没有更多新的处理程序。在上面的示例中,除非我们调用 std::abort()
,否则 outOfMemHandler()
将是 called repeatedly。因此,处理程序要么确保下一次分配成功,要么注册另一个处理程序,或者不注册处理程序,或者不返回(即终止程序)。如果没有新的处理程序并且分配失败,则操作员将抛出异常。
第二部分
鉴于示例中 operator new
的行为,设计良好的 new_handler
必须执行以下操作之一:
提供更多可用内存:这可能允许 operator new 循环内的下一次内存分配尝试成功。实现这一点的一种方法是在程序启动时分配一大块内存,然后在第一次调用 new-handler 时释放它以供程序使用。
安装不同的 new-handler: 如果当前的 new-handler 无法提供更多可用内存,并且有另一个 new-handler 可以,那么当前的 new-handler 可以安装另一个new-handler 代替它(通过调用 set_new_handler
)。下一次 operator new 调用 new-handler 函数时,它将获得最近安装的那个。
(这个主题的一个变体是让 new-handler 修改自己的行为,所以下次调用它时,它会做一些不同的事情。实现这一点的一种方法是让 new-handler 修改静态的、特定于命名空间的或影响新处理程序行为的全局数据。)
卸载新处理程序:这是通过将空指针传递给 set_new_handler
来完成的。如果未安装新处理程序,operator new
将在内存分配不成功时抛出异常((可转换为)std::bad_alloc
)。
抛出异常可转换为 std::bad_alloc
。 operator new
不会捕获此类异常,但会传播到发起内存请求的站点。
不返回:通过调用 abort
或 exit
。
要实现特定于类的 new_handler
,我们必须为类提供自己的 set_new_handler
和 operator new
版本。类的 set_new_handler
允许客户指定类的新处理程序(与标准 set_new_handler
允许客户指定全局新处理程序完全一样)。类的 operator new
确保在为类对象分配内存时,使用特定于类的 new-handler 代替全局 new-handler。
现在我们了解了 new_handler
& set_new_handler
更好的是,我们能够将 要求 #4 适当地修改为:
要求 #4(增强):
我们的 operator new
应该尝试多次分配内存,在每次失败后调用 new-handling 函数。这里的假设是 new-handling 函数可能能够做一些事情来释放一些内存。只有当指向新处理函数的指针是 null
时,operator new
才会抛出异常。
正如所承诺的,来自标准的引用:第 3.7.4.1.3 节:
分配存储失败的分配函数可以调用当前安装的 new_handler(18.4.2.2),如果有的话。 [注意:程序提供的分配函数可以使用 set_new_handler 函数 (18.4.2.3) 获取当前安装的 new_handler 的地址。] 如果使用空异常规范 (15.4) 声明的分配函数 throw() 失败分配存储,它应该返回一个空指针。任何其他分配存储失败的分配函数只能通过抛出类 std::bad_alloc (18.4.2.1) 或从 std::bad_alloc 派生的类的异常来指示失败。
有了 #4 要求,让我们尝试为我们的 new operator
编写伪代码:
void * operator new(std::size_t size) throw(std::bad_alloc)
{
// custom operator new might take additional params(3.7.3.1.1)
using namespace std;
if (size == 0) // handle 0-byte requests
{
size = 1; // by treating them as
} // 1-byte requests
while (true)
{
//attempt to allocate size bytes;
//if (the allocation was successful)
//return (a pointer to the memory);
//allocation was unsuccessful; find out what the current new-handling function is (see below)
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) //If new_hander is registered call it
(*globalHandler)();
else
throw std::bad_alloc(); //No handler is registered throw an exception
}
}
第三部分
请注意,我们不能直接获取新的处理函数指针,我们必须调用 set_new_handler
来找出它是什么。这是粗略但有效的,至少对于单线程代码是这样。在多线程环境中,可能需要某种锁来安全地操作 new-handling 函数背后的(全局)数据结构。 (欢迎对此提供更多引用/详细信息。)
此外,我们有一个无限循环,并且退出循环的唯一方法是成功分配内存,或者让新处理函数执行我们之前推断的事情之一。除非 new_handler
执行其中一项操作,否则 new
运算符内的循环将永远不会终止。
警告:请注意,标准(§3.7.4.1.3
,上面引用的)没有明确说明重载的 new
运算符必须实现无限循环,但它只是说这是默认行为。 所以这个细节可以解释,但大多数编译器(GCC 和 Microsoft Visual C++)确实实现了这个循环功能(您可以编译前面提供的代码示例)。 此外,由于 C++ Scott Meyers等权威人士建议采用这种方法,足够合理。
特殊场景
让我们考虑以下场景。
class Base
{
public:
static void * operator new(std::size_t size) throw(std::bad_alloc);
};
class Derived: public Base
{
//Derived doesn't declare operator new
};
int main()
{
// This calls Base::operator new!
Derived *p = new Derived;
return 0;
}
正如 this 常见问题解答所解释的,编写自定义内存管理器的一个常见原因是优化特定类的对象的分配,而不是针对类或其任何派生类,这基本上意味着Base 类的操作符 new 通常针对大小为 sizeof(Base)
的对象进行调整 - 既不大也不小。
在上面的示例中,由于继承,派生类 Derived
继承了基类的 new 运算符。这使得在基类中调用 operator new 为派生类的对象分配内存成为可能。我们的 operator new
处理这种情况的最佳方法是将此类请求“错误”内存量的调用转移到标准运算符 new,如下所示:
void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base)) // If size is "wrong,", that is, != sizeof Base class
{
return ::operator new(size); // Let std::new handle this request
}
else
{
//Our implementation
}
}
请注意,大小检查也不符合我们的要求#3。这是因为在 C++ 中所有独立对象的大小都不为零,所以 sizeof(Base)
永远不可能为零,所以如果大小为零,请求将被转发到 ::operator new
,并保证它会在符合标准的方式。
引文:From the creator of C++ himself, Dr Bjarne Stroustrup.
实现自定义删除运算符
C++ Standard(§18.4.1.1
) 库将 operator delete
定义为:
void operator delete(void*) throw();
让我们重复收集编写自定义 operator delete
的要求的练习:
要求 #1: 它应返回 void
,其第一个参数应为 void*
。自定义 delete operator
也可以有多个参数,但我们只需要一个参数来传递指向已分配内存的指针。
来自 C++ 标准的引用:
第 3.7.3.2.2 节:
“每个释放函数都应返回 void,其第一个参数应为 void*。释放函数可以有多个参数......”
要求#2:它应该保证删除作为参数传递的空指针是安全的。
来自 C++ 标准的引用:第 3.7.3.2.3 节:
提供给标准库中提供的一个释放函数的第一个参数的值可能是空指针值;如果是这样,对释放函数的调用无效。否则,在标准库中提供给 operator delete(void*) 的值应是先前调用标准库中 operator new(size_t) 或 operator new(size_t, const std::nothrow_t&) 返回的值之一,并且在标准库中提供给 operator delete[](void*) 的值应是先前调用 operator new[](size_t) 或 operator new[](size_t, const std:: 时返回的值之一nothrow_t&) 在标准库中。
要求 #3: 如果传递的指针不是 null
,则 delete operator
应解除分配并分配给指针的动态内存。
来自 C++ 标准的引用:第 3.7.3.2.4 节:
如果给标准库中的释放函数的参数是一个不是空指针值的指针(4.10),则释放函数将释放指针引用的存储,使引用任何部分的所有指针无效。释放的存储空间。
要求 #4: 此外,由于我们的特定类运算符 new 将“错误”大小的请求转发到 ::operator new
,我们必须将“错误大小”的删除请求转发到::operator delete
。
因此,根据我们上面总结的要求,这里是自定义 delete operator
的标准符合伪代码:
class Base
{
public:
//Same as before
static void * operator new(std::size_t size) throw(std::bad_alloc);
//delete declaration
static void operator delete(void *rawMemory, std::size_t size) throw();
void Base::operator delete(void *rawMemory, std::size_t size) throw()
{
if (rawMemory == 0)
{
return; // No-Op is null pointer
}
if (size != sizeof(Base))
{
// if size is "wrong,"
::operator delete(rawMemory); //Delegate to std::delete
return;
}
//If we reach here means we have correct sized pointer for deallocation
//deallocate the memory pointed to by rawMemory;
return;
}
};
free
并假设默认的 operator new
使用 malloc
(或其他)?
std::set_new_handler
的结果。然后我的新处理程序版本将调用旧版本if my version failed to provide any emergency space
。这样,如果另一个库安装了一个新的处理程序,该处理程序将按该库的预期调用。new
在namespace std
中吗?