我该如何编写符合ISO C++标准的自定义new和delete运算符?

71

如何编写符合ISO C++标准的自定义newdelete运算符?

这是关于重载new和delete运算符重载及其后续为什么要替换默认的new和delete运算符?的延续。

第一部分:编写符合标准的new运算符

第二部分:编写符合标准的delete运算符

-

实现自定义删除运算符

_(注:这是作为 [Stack Overflow C++ FAQ](https://stackoverflow.com/questions/tagged/c++-faq) 的条目而存在。如果您想批评以此形式提供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的《More Effective C++》和ISO C++标准的学习。*

3
哇,人们早早地就被downvote了!我猜你甚至还没有完成你的问题?我认为这是一个讨论这类问题的好地方,我给你加1。 - Tom
3
看起来有些人不太喜欢你 :-) 我个人也不喜欢这样的冗长回答,我觉得它应该放在专门的常见问题解答区域,而不是被淹没在每天发布到SO上的成千上万个问题中。但是为了你的努力点一个赞。 - Praetorian
8
我觉得“常见问题”应该包括“答案比您意识到的更有用,尤其是当您经常从事相关工作时”。 - Lightness Races in Orbit
7
但是这个问题经常被问到吗?如果不是,那么虽然我没有反对在这里提出和回答这个问题,但它不应该带有[c++-faq]标签。该标签已经太过嘈杂了。 - James McNellis
4
我同意这个观点。C++ FAQ 不是为所有自问自答的书籍式问答形式所准备的,这是普通用户可以想到的。 - Lightness Races in Orbit
显示剩余15条评论
4个回答

42

第一部分

这个C++ FAQ条目解释了为什么有人想要为自己的类重载newdelete运算符。本FAQ将尝试以符合标准的方式解释如何实现这样的重载。

实现自定义的new运算符

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节的内容:
“分配函数尝试分配所请求的存储空间。如果成功,它将返回一个指向起始地址为所请求大小的存储块的指针...”
标准进一步规定:
“...返回的指针应适当对齐,以便可以将其转换为任何完整对象类型的指针,然后用于访问分配的存储中的对象或数组(直到通过调用相应的释放函数显式释放存储)。即使请求的空间大小为零,请求也可能失败。如果请求成功,返回的值应为非空指针值(4.10),与之前返回的值p1不同,除非该值p1随后被传递给operator delete。”

这给我们带来了进一步的重要要求:

要求 #2:我们使用的内存分配函数(通常是malloc()或其他自定义分配器)应该返回一个适当对齐的指针,该指针可以转换为完整对象类型的指针,并用于访问该对象。

要求 #3:即使请求的字节数为零,我们的自定义操作符new也必须返回一个合法的指针。

其中一个明显的要求甚至可以从new的原型中推断出:

要求 #4:如果new无法分配所请求大小的动态内存,则应抛出std::bad_alloc类型的异常。

但是!这还不止于表面所见:如果你仔细查看new运算符的文档(标准的引用在下面),它声明:

如果使用了set_new_handler来定义一个new_handler函数,那么当标准的默认operator new无法分配所需的存储空间时,会调用这个new_handler函数。
为了理解我们自定义的new如何支持这个要求,我们需要了解以下内容:
什么是new_handlerset_new_handlernew_handler是一个指向接受并返回空值的函数的指针类型,而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()将被调用,程序将在发出错误消息后中止。
在这里需要注意的是,当operator new无法满足内存请求时,它会重复调用new-handler函数,直到它可以找到足够的内存或没有更多的新处理程序。在上面的例子中,除非我们调用std::abort(),否则outOfMemHandler()将会被反复调用。因此,处理程序应该确保下一次分配成功,或注册另一个处理程序,或不注册处理程序,或者不返回(即终止程序)。如果没有新的处理程序并且分配失败,操作员将抛出异常。

继续 1



2
个人而言,我会保存std::set_new_handler的结果。然后我的新处理程序版本将调用旧版本,如果我的版本无法提供任何紧急空间。这样,如果另一个库安装了新处理程序,那么该库将按预期调用它。 - Martin York
1
你确定 newnamespace std 中吗? - Kerrek SB
1
100,000,000 * 4 字节 = 400,000,000 字节 / 1024 = 390625 KiB / 1024 = ~381.47 MiB。在您查看此网页的任何设备上都不太可能失败 :) - Jimmio92
注意:10年过去了,这些链接现在已经失效。我有一个提议的编辑,更新了其中一些链接,但是你的iedone.com解决方案已经找不到了,我也不知道它们最初是什么以修复它。 - Chipster

24

第二部分

...继续阅读

鉴于示例中 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 修改影响其行为的静态、命名空间特定或全局数据。)

卸载 new-handler: 这可通过将空指针传递给 set_new_handler 来完成。如果没有安装 new-handler,则在内存分配失败时,operator new 将抛出一个异常((可转换为) std::bad_alloc)。

抛出一个异常: 可以转换为 std::bad_alloc。这些异常不会被 operator new 捕获,而会传播到请求内存的站点。

不返回: 通过调用 abortexit

要实现类特定的 new_handler,我们必须提供一个具有其自己版本的 set_new_handleroperator new 的类。该类的 set_new_handler 允许客户端为类指定新处理程序(就像标准的 set_new_handler 允许客户端指定全局 new-handler 一样)。类的 operator new 确保在分配类对象的内存时使用类特定的 new-handler 替代全局 new-handler。


既然我们更好地理解了 new_handlerset_new_handler,我们能够适当地修改需求 #4如下:

需求 #4(增强版):
我们的 operator new 应尝试多次分配内存,在每次失败后调用 new-handling 函数。这里的假设是 new-handling 函数可能能够做一些事情来释放一些内存。只有当指向 new-handling 函数的指针为 null 时,operator new 才会抛出异常。

如约,标准中的引用:
第 3.7.4.1.3 节:

  

一个分配函数如果无法分配存储,则可以调用当前已安装的 new_handler18.4.2.2),如果有的话。[注:程序提供的分配函数可以使用 set_new_handler

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

    }

}

第二篇续文


4
您的参考资料是针对C++98标准的,而不是当前的C++11标准。 - Sjoerd
4
截至本次撰写,目前的标准仍然是C++03。但如果你想要来自C++11批准草案的标准,该段落的编号是相同的 - R. Martinho Fernandes
2
@Sjoerd:C++11,目前还不是一个标准,至少不是官方的。因此,目前官方标准仍然是C++03。我会随时添加相关的C++11引用,如果有的话。 - Alok Save
2
@Sjoerd:“我们的operator new应该尝试分配内存超过一次(...)”。还要注意"SHOULD"。这不是一个要求。 - R. Martinho Fernandes
3
@Sjoerd说,FDIS已经通过了。在发布之前,它还不是一个标准。当Herb说“现在它是C++11”时,他是在撒谎。我们拥有的只是C++0x FDIS,其内容与几周后将成为C++11标准的内容相同。 - Lightness Races in Orbit
显示剩余13条评论

20

第三部分

... 继续阅读

请注意,我们无法直接获取新处理程序函数指针,我们必须调用set_new_handler来找出它是什么。这很粗糙但有效,至少对于单线程代码是如此。在多线程环境中,可能需要某种锁来安全地操作新处理函数背后的(全局)数据结构。(欢迎提供更多引文/详细信息。

此外,我们有一个无限循环,唯一的退出循环的方式是成功分配内存,或者新处理函数执行我们之前推断的某些操作之一。除非new_handler执行其中一项操作,否则new运算符内部的循环将永远不会终止。

一个警告: 请注意,标准(上面引用的§3.7.4.1.3)并没有明确说明重载的new运算符必须实现无限循环,而只是说这是默认行为。因此,这个细节是可以解释的,但大多数编译器(GCCMicrosoft 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;
}

FAQ所解释的那样,编写自定义内存管理器的常见原因是为了针对特定类的对象进行优化分配,而不是针对一个类或其任何派生类,这基本上意味着我们的Base类的operator new通常调整为大小为sizeof(Base)的对象——既不大也不小。
在上面的示例中,由于继承,派生类Derived继承了Base类的new运算符。这使得在基类中调用operator new来为派生类的对象分配内存成为可能。我们的operator new处理这种情况的最佳方式是将请求“错误”数量的内存转向标准operator 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,并且它保证将以符合标准的方式处理该请求。
引用:C++之父Bjarne Stroustrup博士

18

实现自定义删除操作符

C++标准库(§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:它应该保证安全地删除传递作为参数的null指针。

引用自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: 此外,由于我们的类特定的operator 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;
        }
};

2
我读完了整篇文章,关于“释放指向rawMemory的内存”的部分... 我应该使用free并假设默认的operator new使用了malloc(或其他)吗? - lmat - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接