【问题标题】:How should I write ISO C++ Standard conformant custom new and delete operators?我应该如何编写符合 ISO C++ 标准的自定义 new 和 delete 运算符?
【发布时间】:2011-11-03 21:07:36
【问题描述】:

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

这是在极具启发性的 C++ 常见问题解答Operator overloading 及其后续Why should one replace default new and delete operators? 中的Overloading new and delete 的延续

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

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

-

Implementing Custom delete operator

_(注意:这是 [Stack Overflow 的 C++ 常见问题解答](https://stackoverflow.com/questions/tagged/c++-faq) 的一个条目。如果您想批评以这种形式提供常见问题解答的想法,然后[开始这一切的元数据发布](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++ 标准的学习。*

【问题讨论】:

  • 哇,人们很早就开始投反对票了! - 我猜你还没问完你的问题?我认为这是讨论此类问题的好地方,请向我 +1。
  • @Als 看起来有些人不太喜欢你 :-) 我个人不喜欢像这样漫无边际的答案,我觉得它属于某个专门的常见问题解答部分在每天发布到 SO 的数千个问题中迷失方向。但是 +1 的努力。
  • 我认为“常见问题”可能还包括“当您经常进行相关工作时,知道的答案比您想象的更有用”
  • 但是这个问题经常被问到吗?如果不是,那么虽然我不反对这里提出和回答的问题,但它不应该有 [c++-faq] 标签。标签已经太吵了。
  • 其实我同意这一点。 c++-faq 并不是普通用户能想到的每一个自答书式问答。

标签: c++ operator-overloading new-operator c++-faq delete-operator


【解决方案1】:

第一部分

This C++ FAQ entry 解释了为什么人们可能想要为自己的类重载newdelete 运算符。本常见问题解答试图解释如何以符合标准的方式这样做。

实现自定义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) p0,不同于任何先前返回的值 p1,除非该值 p1 随后被传递给操作员 delete

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

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

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

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

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

但是! 这比表面上看到的要多:如果您仔细查看 new 运算符 documentation(来自标准的引文如下),它指出:

如果 set_new_handler 已用于定义 new_handler 函数,则此 new_handler 函数将由 operator new 的标准默认定义调用,如果它无法自行分配请求的存储空间。

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

new_handlerset_new_handler 是什么?

new_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() 将被调用,程序将在issuing an error message 之后中止。

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

Continuation 1


【讨论】:

  • 我个人会保存std::set_new_handler 的结果。然后我的新处理程序版本将调用旧版本if my version failed to provide any emergency space。这样,如果另一个库安装了一个新的处理程序,该处理程序将按预期被该库调用。
  • 您确定newnamespace std 中吗?
  • 100,000,000 * 4 字节 = 400,000,000 字节 / 1024 = 390625 KiB / 1024 = ~381.47 MiB。很可能不会在您可以查看此网页的任何内容上失败:)
【解决方案2】:

第二部分

... continued

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

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

安装不同的 new-handler: 如果当前的 new-handler 不能提供更多可用内存,并且有另一个 new-handler 可以,那么当前的 new-handler 可以在其位置安装另一个新处理程序(通过调用set_new_handler)。下一次 operator new 调用 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 允许客户端指定全局新处理程序完全一样)。类的operator new 确保在为类对象分配内存时使用特定于类的new-handler 代替全局new-handler。


现在我们更好地理解了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),如果有的话。 [注意:程序提供的分配函数可以使用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

    }

}

Continuation 2

【讨论】:

  • 您引用的是 C++98 标准,而不是当前的 C++11 标准。
  • @Sjoerd:在撰写本文时,当前标准仍然是 C++03。但是如果你想要一个来自 C++11 批准的草案,段落编号是一样的
  • @Sjoerd:C++11,目前还不是标准,至少不是正式的。所以目前官方标准还是C++03。我不介意在跟踪它们时添加相关的 C++11 引号。
  • @Sjoerd:“我们的 operator new 应该尝试多次分配内存 (...)”。另请注意"SHOULD"。不是要求。
  • @Sjoerd:FDIS 获得批准。在发布之前,它不是标准。当 Herb 说“现在是 C++11”时,他是在撒谎。我们所拥有的只是 C++0x FDIS,它的内容与 在几周后成为 C++11 标准的内容相同。
【解决方案3】:

第三部分

... continued

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

此外,我们有一个无限循环,而退出循环的唯一方法是成功分配内存,或者让新处理函数执行我们之前推断的事情之一。除非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;
}

正如 this 常见问题解答所解释的,编写自定义内存管理器的一个常见原因是优化特定类的对象的分配,而不是类或任何 它的派生类,这基本上意味着我们的基类运算符 new 通常针对大小为 sizeof(Base) 的对象进行调整 - 没有更大也没有更小。

在上面的示例中,由于继承,派生类Derived 继承了基类的新运算符。这使得在基类中调用 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.

【讨论】:

    【解决方案4】:

    实现自定义删除操作符

    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:它应该保证删除作为参数传递的空指针是安全的。

    来自 C++ 标准的引用: 第 3.7.3.2.3 节:

    提供给标准库中提供的一个释放函数的第一个参数的值可能是空指针值;如果是这样,对释放函数的调用无效。否则,提供给标准库中operator delete(void*) 的值应是先前调用标准库中的operator new(size_t)operator new(size_t, const std::nothrow_t&amp;) 返回的值之一,以及提供给标准库中operator delete[](void*) 的值应该是之前在标准库中调用 operator new[](size_t)operator new[](size_t, const std::nothrow_t&amp;) 返回的值之一。

    要求 #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;
            }
    };
    

    【讨论】:

    • 我阅读了整篇文章的“释放 rawMemory 指向的内存”部分...我应该使用free 并假设默认的operator new 使用malloc(或其他) ?
    猜你喜欢
    • 1970-01-01
    • 2023-03-23
    • 1970-01-01
    • 1970-01-01
    • 2013-03-12
    • 1970-01-01
    • 2012-08-04
    • 2013-08-10
    • 2019-02-09
    相关资源
    最近更新 更多