【问题标题】:Sized deallocation in C++: What is the correct behaviour of the global operator delete(void* ptr, std::size_t size)C++ 中的大小释放:全局运算符 delete(void* ptr, std::size_t size) 的正确行为是什么
【发布时间】:2018-03-28 06:17:42
【问题描述】:

我不确定我是否正确理解了 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 标准,但我认为我没有找到任何关于全局和类本地运算符应该如何表现的具体内容(这可能只是我在解释标准的措辞,因为我不是母语人士)。

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

正确的行为应该是什么?

【问题讨论】:

  • 请注意,编译器不需要使用用户提供的newdelete(和/或根本不使用newdelete),如果它可以证明它没有'不要改变程序的语义。
  • Clang 需要-fsized-deallocation,然后它与 GCC 一致。但是,看起来 MSVC 在这里得到了它。

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


【解决方案1】:

我相信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,您实际上要求编译器忽略动态 类型,仅指当前作用域中的 static 类型,大小为 16 字节。 你已经选择了一个删除函数,所以编译器不需要动态查找一个。

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-05-28
    • 2019-10-23
    • 2012-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-02
    相关资源
    最近更新 更多