C++中的大小重新分配:全局运算符delete的正确行为是什么(void*ptr,std :: size_t size)

TG-*_*SAG 8 c++ memory delete-operator c++14

我不确定我是否在C++中正确理解了"大小的释放".在C++ 14中,以下签名被添加到全局范围:

void operator delete(void* ptr, std::size_t size) noexcept
Run Code Online (Sandbox Code Playgroud)

我正在使用GCC 7.1.0编译以下源代码:

#include <cstdio>   // printf()
#include <cstdlib>  // exit(),malloc(),free()
#include <new>      // new(),delete()

void* operator new(std::size_t size)
{
    std::printf("-> operator ::new(std::size_t %zu)\n", size);
    return malloc(size);
}

void operator delete(void* ptr) noexcept
{
    std::printf("-> operator ::delete(void* %p)\n", ptr);
    free(ptr);
}

void operator delete(void* ptr, std::size_t size) noexcept
{
    std::printf("-> operator ::delete(void* %p, size_t %zu)\n", ptr, size);
    free(ptr);
}


struct B
{
    double d1;
    void* operator new(std::size_t size)
    {
        std::printf("-> operator B::new(std::size_t %zu)\n", size);
        return malloc(size);
    };

    void operator delete(void* ptr, std::size_t size)
    {
        std::printf("-> operator B::delete(void* %p, size_t %zu)\n", ptr, size);
        free(ptr);
    };

    virtual ~B()
    {
        std::printf("-> B::~B()");
    }
};


struct D : public B
{
    double d2;
    virtual ~D()
    {
        std::printf("-> D::~D()");
    }
};

int main()
{

    B *b21 = new B();
    delete b21;

    B *b22 = new D();
    delete b22;

    D *d21 = new D();
    delete d21;

    std::printf("*****************************\n");

    B *b11 = ::new B();
    ::delete b11;

    B *b12 = ::new D();
    ::delete b12;

    D *d11 = ::new D();
    ::delete d11;

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我得到以下输出:

-> operator B::new(std::size_t 16)
-> B::~B()-> operator B::delete(void* 0x16e3010, size_t 16)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 0x16e3010, size_t 24)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 0x16e3010, size_t 24)
*****************************
-> operator ::new(std::size_t 16)
-> B::~B()-> operator ::delete(void* 0x16e3010, size_t 16)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 0x16e3010, size_t 16)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 0x16e3010, size_t 24)
Run Code Online (Sandbox Code Playgroud)

MS Visual Studio 2017为我提供了以下输出:

-> operator B::new(std::size_t 16)
-> B::~B()-> operator B::delete(void* 0081CDE0, size_t 16)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 00808868, size_t 24)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 00808868, size_t 24)
*****************************
-> operator ::new(std::size_t 16)
-> B::~B()-> operator ::delete(void* 0081CDE0, size_t 16)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 00808868, size_t 24)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 00808868, size_t 24)
Run Code Online (Sandbox Code Playgroud)

而Clang 5.0甚至没有调用全局大小的释放operator delete(只有operator delete一个参数).正如评论部分Clang中提到的TC需要额外的参数-fsized-deallocation来使用大小分配,结果将与GCC相同:

-> operator B::new(std::size_t 16)
-> B::~B()-> operator B::delete(void* 0x219b6c0, size_t 16)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 0x219b6c0, size_t 24)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 0x219b6c0, size_t 24)
*****************************
-> operator ::new(std::size_t 16)
-> B::~B()-> operator ::delete(void* 0x219b6c0, size_t 16)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 0x219b6c0, size_t 16)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 0x219b6c0, size_t 24)
Run Code Online (Sandbox Code Playgroud)

对我来说VS2017似乎有正确的行为,因为我对类特定运算符的理解是使用派生类的大小,即使在基类指针上调用了delete.我希望通过调用全局来实现对称行为operator delete.

我已经查看了ISO C++ 11/14标准,但我认为我没有找到关于全局和类本地运算符应该如何表现的任何具体信息(可能只是我在解释其中的措辞时遇到问题)标准,因为我不是母语人士.

有人可以详细说明这个话题吗?

什么应该是正确的行为?

cyb*_*son 2

我相信为您的运算符添加前缀的双冒号运算delete符正在规避 \xe2\x80\x9c Correct\xe2\x80\x9doperator delete()。我已经在 GCC、Clang 和 Intel\xe2\x80\x99s 编译器上练习了代码,它们都同意delete应该向运算符发送 16 字节大小。这是因为他们似乎将 C++ 规范解释为您已明确要求全局范围的删除函数,而忽略任何动态调度。稍后会详细介绍这一点。

\n

\xe2\x80\x99s 发生了什么

\n

首先,让\xe2\x80\x99s 稍微调整一下你的原始代码以消除一些变量:

\n
struct B\n{\n    double d1;\n    virtual ~B() = default;\n};\n\nstruct D : public B\n{\n    double d2;\n};\n\nint main()\n{\n    B *b01 = new D();\n    ::delete b01; // 1: The "problem" case.\n\n    D *d01 = new D();\n    ::delete d01; // 2: The "problem" case (sanity check).\n\n    B *b02 = ::new D();\n    delete b02;   // 3: Typical deletion.\n\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

实际上,不需要任何重写即可表现出此行为。我们可以查看发出的程序集,看看\xe2\x80\x99s 发生了什么。默认情况下,GCC 似乎使用 sizeddelete运算符,因此上面的内容很有趣(我用 GCC 11 编译-O0)。正如您所注意到的,编译器通过了sizeof(*b01)给删除函数:

\n
    mov     rdx, QWORD PTR [rax]\n    sub     rdx, 16\n    mov     rdx, QWORD PTR [rdx]\n    lea     rbx, [rax+rdx]\n    mov     rdx, QWORD PTR [rax]\n    mov     rdx, QWORD PTR [rdx]\n    mov     rdi, rax\n    call    rdx\n    mov     esi, 16  // Passed as the size to delete().\n    mov     rdi, rbx\n    call    operator delete(void*, unsigned long)\n
Run Code Online (Sandbox Code Playgroud)\n

...本质上,查找虚拟析构函数,调用它,然后调用大小为的删除函数*b01(注意:在标准库情况下,这可能没问题,因为堆知道分配实际有多大,并且会充分收获)。

\n

为了确认我们\xe2\x80\x99正在寻找当前范围内静态的大小,我添加了示例2,它发出sizeof(*d01)到第二个参数:

\n
    call    rdx\n    mov     esi, 24  // Passed as the size to delete().\n    mov     rdi, rbx\n    call    operator delete(void*, unsigned long)\n
Run Code Online (Sandbox Code Playgroud)\n

真正有趣的地方是 \xe2\x80\x9cnormal\xe2\x80\x9d 案例,示例 3:

\n
    mov     rdx, QWORD PTR [rax]\n    add     rdx, 8   // Offset 8 in the vtable for b02.\n    mov     rdx, QWORD PTR [rdx]\n    mov     rdi, rax\n    call    rdx\n
Run Code Online (Sandbox Code Playgroud)\n

在这里,它在 vtable 中查找b02,并找到 \xe2\x80\x9c 删除析构函数。\xe2\x80\x9d 这是一个包装了我们通常认为的析构函数的函数(因为它的 \xe2\x80\x99s 位于vtable,我们\xe2\x80\x99将找到它\xe2\x80\x99s) 的D,并调用delete在该函数执行后调用该运算符。例如:

\n
    // (Prolog omitted.)\n    call    D::~D() // [complete object destructor]\n    mov     rax, QWORD PTR [rbp-8]\n    mov     esi, 24\n    mov     rdi, rax\n    call    operator delete(void*, unsigned long)\n
Run Code Online (Sandbox Code Playgroud)\n

因此,我们对析构函数进行了虚拟查找,运行了正确的析构函数,然后delete运算符为其\xe2\x80\x99 的第二个参数获取了 24 字节大小。

\n

C++ 规范的论证

\n

如果我们看一下 C++(在本例中为 C++14)规范 \xc2\xa712.5.4(免费商店),它指出:

\n
\n

特定于类的释放函数查找是通用释放函数查找(5.3.5)的一部分,发生如下。如果delete表达式用于释放静态类型具有虚拟析构函数的类对象,则释放函数是在动态类型\xe2\x80\x99s虚拟析构函数(12.4)定义时选择的函数。否则,如果使用delete表达式来释放类T或其数组的对象,则该对象的静态和动态类型应相同,并且在 的范围内查找释放函数\xe2\x80\x99s 的名称T。如果此查找未能找到名称,则常规释放函数查找(5.3.5)将继续...

\n
\n

换句话说(我的解释是),当您为 定义虚拟析构函数时B,您定义了一个隐式operator delete,但是通过调用::delete,您实质上要求编译器忽略动态类型,并仅引用当前作用域中的静态类型,这大小为 16 字节。 您选择了删除函数,因此\xe2\x80\x99s不需要编译器动态查找。

\n

再次在 \xc2\xa75.3.5.9 中(删除):

\n
\n

当删除表达式delete中的关键字前面带有一元时::运算符时,将在全局范围内查找释放函数\xe2\x80\x99s 名称。否则,查找会考虑特定于类的释放函数(12.5)。如果未找到特定于类的释放函数,则在全局范围内查找释放函数\xe2\x80\x99s 名称。

\n
\n

换句话说,\xe2\x80\x9cyou 要求全局函数,所以我跳过了查找类特定函数的部分。\xe2\x80\x9d

\n

有人可能会说 MSVC 行为也是有效的,因为通过这一切,没有任何东西明确表明传递给删除函数的大小与函数本身不可避免地相关。 当然,MSVC 行为还使编码员不必在未定义行为雷区中导航另一个雷区,因为编译器设法从某处获取实际正确的大小。然而,查看 GCC 发出的代码,在显式调用全局范围的删除函数时,\xe2\x80\x9cdifficult\xe2\x80\x9d 会很难收集正确的大小。

\n