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

时间:2017-10-16 16:44:07

标签: c++ memory c++14 delete-operator

我不确定我是否理解"大小的释放"在C ++中正确使用。 在C ++ 14中,以下签名was added到全局范围:

void operator delete(void* ptr, std::size_t size) noexcept

我使用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;
}

我得到以下输出:

-> 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)

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)

而且Clang 5.0甚至没有调用全局大小的释放operator delete(仅使用一个参数的operator delete)。作为T.C.在评论部分中提到Clang需要额外的参数-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)

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

我已经查看了ISO C ++ 11/14标准,但我认为我没有发现任何关于全局和类本地运营商应该如何表现的具体信息(可能只是我有问题解释标准的措辞,因为我不是母语人士。

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

正确的行为应该是什么?

1 个答案:

答案 0 :(得分:0)

我相信在您的 delete 运算符前加上双冒号运算符是在绕过“正确的”operator delete()。我已经在 GCC、Clang 和 Intel 的编译器上练习了代码,他们都同意 delete 运算符应该发送 16 字节大小。这是因为他们似乎将 C++ 规范解释为说您已明确要求全局范围的删除功能,而忽略任何动态分派。稍后会详细介绍。

发生了什么

首先,让我们稍微调整一下您的原始代码以消除一些变量:

struct B
{
    double d1;
    virtual ~B() = default;
};

struct D : public B
{
    double d2;
};

int main()
{
    B *b01 = new D();
    ::delete b01; // 1: The "problem" case.

    D *d01 = new D();
    ::delete d01; // 2: The "problem" case (sanity check).

    B *b02 = ::new D();
    delete b02;   // 3: Typical deletion.

    return 0;
}

实际上,不需要任何覆盖来表现这种行为。我们可以查看发出的程序集,看看发生了什么。默认情况下,GCC 似乎使用了 sized delete 操作符,所以上面很有趣(我用 GCC 11 编译,-O0)。如您所见,编译器将 sizeof(*b01) 传递给删除函数:

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

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

为了确认我们正在静态地查找当前作用域中的大小,我添加了示例 2,它将 sizeof(*d01) 发送到第二个参数中:

    call    rdx
    mov     esi, 24  // Passed as the size to delete().
    mov     rdi, rbx
    call    operator delete(void*, unsigned long)

真正有趣的是在“正常”情况下,示例 3:

    mov     rdx, QWORD PTR [rax]
    add     rdx, 8   // Offset 8 in the vtable for b02.
    mov     rdx, QWORD PTR [rdx]
    mov     rdi, rax
    call    rdx

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

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

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

来自 C++ 规范的证明

如果我们查看 C++(在本例中为 C++14)规范,§12.5.4(免费存储),它指出:

<块引用>

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

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

同样,在 §5.3.5.9(删除)中:

<块引用>

delete-expression 中的关键字 delete 前面是一元 :: 运算符时,会在全局范围内查找释放函数的名称。否则,查找会考虑特定于类的释放函数 (12.5)。如果未找到特定于类的释放函数,则在全局范围内查找释放函数的名称。

另一种说法,“您要求的是全局函数,所以我跳过了查找特定于类的函数的部分。”

有人可能会争辩说 MSVC 的行为也是有效的,因为通过所有这些,没有任何内容明确说明传递给 delete 函数的大小与函数本身无情地联系在一起。 当然,还有,MSVC 行为使编码人员不必在未定义行为雷区中导航另一个地雷,因为编译器设法从某处获取实际正确的大小。然而,查看从 GCC 发出的代码,在显式调用全局范围的删除函数的同时收集正确的大小将是“困难的”。