我应该如何编写符合ISO C++标准的自定义新的和删除操作符?

Alo*_*ave 62 c++ operator-overloading c++-faq new-operator delete-operator

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

这是在延续重载new和delete在非常照明C++ FAQ,操作符重载,以及其后续,一个为什么要更换默认的new和delete操作?

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

第2节:编写符合标准的delete运算符

(注意:这是Stack Overflow的C++常见问题解答的一个条目.如果你想批评在这种形式下提供常见问题解答的想法,那么发布所有这些的元数据的发布将是这样做的地方.这个问题在C++聊天室中受到监控,其中FAQ的想法首先开始,所以你的答案很可能被那些提出想法的人阅读.)
注意:答案是基于Scott Meyers的学习'更有效的C++和ISO C++标准.

Alo*_*ave 34

第一部分

这个C++ FAQ条目解释了为什么人们可能想要为自己的类重载newdelete运算符.本篇FAQ试图解释一个人如何以符合标准的方式这样做.

实现自定义new运算符

C++标准(第18.4.1.1节)定义operator new为:

void* operator new (std::size_t size) throw (std::bad_alloc);
Run Code Online (Sandbox Code Playgroud)

C++标准规定了这些运算符的自定义版本在§3.7.3和§18.4.1中必须遵守的语义

让我们总结一下这些要求.

要求#1:它应动态分配至少size字节的内存并返回指向已分配内存的指针.引自C++标准的3.7.4.1.3节:

分配功能尝试分配所请求的存储量.如果成功,它将返回一个存储块的起始地址,其长度以字节为单位应至少与请求的大小一样大......

该标准进一步强加:

...返回的指针应适当对齐,以便它可以转换为任何完整对象类型的指针,然后用于访问分配的存储中的对象或数组(直到通过调用相应的方式显式释放存储空间)解除分配功能).即使请求的空间大小为零,请求也可能失败.如果请求成功,则返回的值应为非空指针值(4.10)p0,与先前返回的值p1不同,除非该值p1随后传递给运算符delete.

这为我们提供了更多重要要求:

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

要求#3:new即使请求零字节,我们的自定义运算符也必须返回合法指针.

new原型中可以推断出一个明显的要求是:

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

但!除此之外还有更多内容:如果您仔细查看new操作员文档(标准的引用,请进一步说明),它指出:

如果set_new_handler已被用来定义一个new_handler功能,这个new_handler功能是通过标准默认定义所谓的operator new,如果它不能由它自己的分配请求的存储空间.

要了解我们的自定义new需要如何支持此要求,我们应该了解:

什么是new_handlerset_new_handler

new_handler是一个指向函数的指针的typedef,该函数获取并不返回任何内容,并且 set_new_handler是一个获取并返回a的函数new_handler.

set_new_handler的参数是一个指向函数运算符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;
}
Run Code Online (Sandbox Code Playgroud)

在上面的例子中,operator new(最可能)将无法100,000,000整数分配空间,并且该函数outOfMemHandler()将被调用,并且程序将后中止发出一条错误消息.

这里要注意的是,当它是重要的operator new是无法满足内存请求时,它调用的new-handler函数,直到它可以找到足够的内存或没有更多的新的处理程序.在上面的例子中,除非我们调用std::abort(),outOfMemHandler()重复调用.因此,处理程序应该确保下一个分配成功,或者注册另一个处理程序,或者不注册处理程序,或者不返回(即终止程序).如果没有新的处理程序并且分配失败,则操作员将抛出异常.

延续1


  • 就我个人而言,我会保存“std::set_new_handler”的结果。然后,我的新处理程序版本将调用旧版本“如果我的版本无法提供任何紧急空间”。这样,如果另一个库安装了一个新的处理程序,该处理程序将按预期被该库调用。 (2认同)

Alo*_*ave 21

第二部分

... 继续

鉴于operator new示例中的行为,精心设计的new_handler 必须执行以下操作之一:

提供更多可用内存:这可以允许运算符new循环内的下一次内存分配尝试成功.实现此目的的一种方法是在程序启动时分配大块内存,然后在第一次调用new-handler时释放它以在程序中使用.

安装一个不同的新处理程序:如果当前的新处理程序无法使更多的内存可用,并且还有另一个可以的新处理程序,那么当前的new-handler可以在其位置安装另一个new-handler(通过电话set_new_handler).下一次operator new调用new-handler函数时,它将获得最近安装的函数.

(这个主题的一个变体是新的处理程序来修改它自己的行为,所以下次调用它时,它会做一些不同的事情.实现这一点的一种方法是让new-handler修改static,namespace-specific,或者影响新处理程序行为的全局数据.)

卸载new-handler: 这是通过传入一个空指针来完成的set_new_handler.如果没有安装新处理程序,operator newstd::bad_alloc在内存分配不成功时会抛出异常((可转换为)).

抛出一个可兑换的例外std::bad_alloc.此类异常不会被捕获operator new,但会传播到发起内存请求的站点.

不归还:通过电话abortexit.

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


现在我们明白new_handlerset_new_handler我们就能够更好修改要求#4适当为:

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

正如所承诺的那样,标准引用:
第3.7.4.1.3节:

无法分配存储的分配函数可以调用当前安装的new_handler(18.4.2.2)(如果有).[注意:程序提供的分配函数可以new_handler使用set_new_handler函数(18.4.2.3)获取当前安装的地址.]如果使用空的exception-specification(15.4)声明的分配函数throw()无法分配存储,则应返回空指针.任何其他无法分配存储的分配函数都只能通过抛出class 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

    }

}
Run Code Online (Sandbox Code Playgroud)

延续2

  • @Sjoerd:在撰写本文时,**当前的**标准仍然是C++ 03.但是如果你想要一个来自C++ 11批准的草案,段落号*是相同的*. (3认同)
  • 您的参考是C++ 98标准,而不是当前的C++ 11标准. (2认同)
  • @Sjoerd:**C++11**,还不是标准,**至少不是正式的**。所以目前官方标准仍然是**C++03**。我不介意在跟踪相关 **C++11** 引号时添加它们。 (2认同)
  • @Sjoerd:“我们的运算符 new 应该尝试多次分配内存(...)”。另请注意[“应该”](http://www.ietf.org/rfc/rfc2119.txt)。不是要求。 (2认同)
  • @Sjoerd:FDIS获得批准.它在发布之前不是标准.当赫伯说"现在是C++ 11"时,他说谎了.我们所拥有的只是C++ 0x FDIS,其内容与几周内的_will_是C++ 11标准相同. (2认同)

Alo*_*ave 18

第三部分

... 继续

请注意,我们无法直接获取新的处理函数指针,我们必须调用set_new_handler以找出它是什么.这是粗略但有效的,至少对于单线程代码而言.在多线程环境中,可能需要某种锁来安全地操纵新处理函数后面的(全局)数据结构.(欢迎更多引用/细节.)

此外,我们有一个无限循环,循环的唯一方法是成功分配内存,或者新处理函数执行我们之前推断的事情之一.除非new_handler做其中之一,否则new运算符内的循环将永远不会终止.

需要 注意的是:请注意,标准(§3.7.4.1.3上面引用的)没有明确说明重载new运算符必须实现无限循环,但它只是说这是默认行为.所以这个细节有不同的解释,但大多数编译器(的GCC微软的Visual C++)国家执行这个循环功能(你可以编译前面提供的代码示例).此外,由于像Scott Meyers这样的C++作者提出了这种方法,因此它足够合理.

特殊情况

让我们考虑以下场景.

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;
}
Run Code Online (Sandbox Code Playgroud)

正如常见问题所解释的那样,编写自定义内存管理器的一个常见原因是优化特定类对象的分配,而不是对类或其任何派生类的分配,这基本上意味着我们的Base类新操作符通常是调整大小的物体 - sizeof(Base)没有更大,没有更小.

在上面的示例中,由于继承,派生类Derived继承了Base类的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
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,检查尺寸也符合我们的要求#3.这是因为所有独立对象在C++中都具有非零大小,因此sizeof(Base)永远不能为零,因此如果大小为零,请求将被转发到::operator new,并且保证它将以标准兼容的方式处理它.

引用:来自C++自己的创造者,Bjarne Stroustrup博士.


Alo*_*ave 14

实现自定义删除操作符

C++ Standard(§18.4.1.1)库定义operator delete为:

void operator delete(void*) throw();
Run Code Online (Sandbox Code Playgroud)

让我们重复一下收集编写习惯的要求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;
        }
};
Run Code Online (Sandbox Code Playgroud)

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