当类版本可用时引用的全局运算符delete []

时间:2017-07-22 16:00:58

标签: c++ visual-c++ c++14

请考虑以下代码:

#include <cstdlib>

class Base
{
public:
    virtual ~Base() noexcept = default;

    void* operator new[](size_t count) { return std::malloc(count); }
    void operator delete[](void* ptr) { std::free(ptr); }
};

class Derived : public Base
{
};

int main()
{
    auto ptr = new Derived[100];
    delete[] ptr;
}

在没有C ++运行时实现的环境(例如使用/NODEFAULTLIB开关)中使用最新版本的MSVC进行编译会引发全局operator delete[]的未解决的外部错误,即使有一个类版本:

error LNK2019: unresolved external symbol "void __cdecl operator delete[](void *)" (??_V@YAXPEAX@Z) referenced in function main

但是,当使用 C ++运行时编译时,将按预期调用运算符的类版本。

这里发生了什么?

1 个答案:

答案 0 :(得分:4)

TL; DR

当分配的数组长度为0时,将调用运算符的全局版本。编译器无法事先知道数组的大小(毕竟它是动态的),因此它会检查0长度以防万一。

挖掘

要回答这个问题,我们必须深入研究拆解。这是相关位(由Compiler Explorer与Visual C ++编译器的19.10.25017版本生成),带有一些额外的注释:

        ; Bail if we're deleting nullptr
        test     rbx, rbx
        je       SHORT $LN6@main

        ; Subtract sizeof(size_t) from the pointer and compare to 0.
        ; If 0, jump to the label below.
        lea      rcx, QWORD PTR [rbx-8]
        cmp      QWORD PTR [rcx], 0
        je       SHORT $LN5@main

        ; Load the vtbl address from the first element
        mov      rax, QWORD PTR [rbx]

        ; Set the second parameter to be some flags
        mov      edx, 3

        ; Set the first parameter to be the pointer
        mov      rcx, rbx

        ; Call destructor through the vtbl
        call     QWORD PTR [rax]

        ; All done
        jmp      SHORT $LN6@main

$LN5@main:
        call     ??_V@YAXPEAX@Z   ; operator delete[]

$LN6@main:
        ; Function epilogue...

从反汇编中可以看出,仅当数组之前存储的数字为0时才调用全局operator delete[]。可以肯定的是,这个数字是分配的大小或数字元素,但我们可以通过再次查看反汇编来确认这一点:

        ; Allocate 100 * sizeof(Derived) + sizeof(size_t)
        mov      ecx, 808
        call     malloc

        ; ...

        mov      r8d, 100

        ; ...

        mov      QWORD PTR [rax], r8

还清楚为什么这个检查是必要的 - 如果我们分配了一个0长度的数组,我们就不能取消引用第一个元素来调用析构函数。更改样本以分配空数组会产生对全局operator new[]的调用,因此所有内容都匹配。

所以你有它。我不知道是否可以避免对全局运算符的调用,但至少现在我们知道它存在的原因。

进一步探索

尽管如此,情况还是有点不对劲。难道这不是通过调用类operator new[]来实现的,完全消除了这个问题吗?还有一件事:在非0长度的情况下,对“operator delete[]”类的调用在哪里?

让我们先回答第二个问题。调用发生在Derived::`vector deleting destructor'内,它是用户定义的析构函数的包装器。看看反汇编,我们可以看到:

  1. 直接调用对象的“真实”析构函数,然后紧跟“operator delete”类或
  2. 遍历数组,为每个元素调用析构函数,然后调用类“operator delete[]
  3. (使用传递给析构函数的标志确定是在单个对象上还是在数组上执行销毁。)

    好的,这样就可以在存在动态多态的情况下调用正确版本的释放函数(因为函数声明为static)。来自C ++ 14标准的§12.5.4:

      

    ...如果 delete-expression 用于解除分配类对象   其静态类型具有虚拟析构函数,即释放函数   是在动态类型的定义点选择的那个   虚拟析构函数......

    然而,在§5.3.5.3中它说:

      

    ...在第二个备选方案(删除数组)中,如果是动态类型的话   要删除的对象与其静态类型不同,行为是   未定义。

    似乎微软的实现允许对数组进行多态删除,这在UB下完全有效。

    另一方面,Clang和GCC几乎完成了你对阅读标准的期望。这是GCC 7.1的输出:

    .L13:
            ; Loop over the array and call destructors
            cmp     rbx, QWORD PTR [rbp-40]
            je      .L12
            sub     rbx, 8
            mov     rax, QWORD PTR [rbx]
            mov     rax, QWORD PTR [rax]
            mov     rdi, rbx
            call    rax
            jmp     .L13
    .L12:
            ; Call the class' operator delete[]
            mov     rax, QWORD PTR [rbp-40]
            sub     rax, 8
            mov     rdi, rax
            call    Base::operator delete[](void*)
    

    此代码来自main,而不是来自任何析构函数包装器。由于编译器可以假定程序不调用UB,因此它可以生成直接遍历数组的代码(因为每个对象的大小在编译时都是已知的)并调用operator delete[]的正确版本。当然,如果我们将new的结果存储在Base*变量中,那么删除就会被删除。

    因此,可以仅使用释放函数的类版本来实现数组删除。我不知道为什么微软选择以他们的方式实现数组删除,但由于这种实现,我们现在被迫提供全局分配和释放功能,或者找到其他方法来分配数组。