析构函数调用的次数超过了应有的次数。是否存在 MSVC 错误?

Destructor called more times than it should. Bug in MSVC or not?

这个节目:

#include <iostream>

using namespace std;

struct B {
    B() { cout << "B"; }
    //B(const B& b) { cout << "copyB"; }
    ~B() { cout << "~B"; }
};

struct C : B {

};

void f(B b) {

}

int main() {
    C c;
    f(c);
    return 0;
}

输出B~B~B~B,即调用析构函数三次,为什么?

仅在 MSVC 中。 Clang 和 GCC 输出 B~B~B(这很可能是正确的)。

有趣的是:如果你取消注释 copy-ctor,它输出 BcopyB~B~B,这是正确的(析构函数被调用两次)。

是MSVC编译器的bug吗?或者这是正确的行为?

(Visual Studio2019最新,cl.exe版本19.28.29337)

如果打印地址:

#include <stdio.h>

struct B {
    B() {  printf(" B() <%p>\n", (void*)this); }
    ~B() { printf("~B() <%p>\n", (void*)this); }
};

struct C : B { };

void f(B b) { }

int main() {
    C c;
    f(c);
}

输出为:

 B() <000000A013FFFAC4>
~B() <000000A013FFFAA0>
~B() <000000A013FFFBA4>
~B() <000000A013FFFAC4>

如您所见,没有对象被破坏 2 次。似乎有一个临时参与,据我所知,这是不允许的。这使我相信这是一个错误。只有当复制构造函数很简单时才会发生这种情况。由于析构函数不是微不足道的,它是可观察到的行为,并且不在 a​​s-if 规则的涵盖范围内。

在禁用优化的情况下查看 Godbolt 的编译器资源管理器 compilation result,请参阅此 main 函数:

        sub     rsp, 56                             ; 00000038H
        lea     rcx, QWORD PTR c$[rsp]
        call    C::C(void)
        npad    1
        movzx   eax, BYTE PTR $T2[rsp]
        mov     BYTE PTR $T1[rsp], al
        movzx   ecx, BYTE PTR $T1[rsp]
        call    void f(B)                     ; f
        npad    1
        lea     rcx, QWORD PTR $T2[rsp]
        call    B::~B(void)                     ; B::~B
        npad    1
        lea     rcx, QWORD PTR c$[rsp]
        call    C::~C(void)
        xor     eax, eax
        add     rsp, 56                             ; 00000038H
        ret     0

所以 c$[rsp] 在堆栈上是变量 c。这里被切片成B类型的临时$T2[rsp],然后复制到临时$T1[rsp],然后$T1[rsp]传到f(B)那里销毁,$T2[rsp]在这里被销毁了。

同样值得注意的是,B 的复制是通过 al 寄存器完成的,但是将 C 切片到 B 是空操作。对于 class 没有成员的情况,两者看起来都很好;我们添加成员,我们将看到将 C 切片到 b 和复制 B。

我不确定标准是否允许或禁止为转换(切片)和按值传递制作单独的副本,但这似乎是这里发生的事情。

结论是MSVC的一个bug。虽然标准规定在将参数传递给函数时可以创建一个临时对象,但是,应该遵守许多限制。相关规则写在下面:
[class.temporary#3]

When an object of class type X is passed to or returned from a function, if X has at least one eligible copy or move constructor ([special]), each such constructor is trivial, and the destructor of X is either trivial or deleted, implementations are permitted to create a temporary object to hold the function parameter or result object. The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the eligible trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object).

在您的示例中,传递的参数是 class C 类型,它派生自具有非用户提供的副本的基 class B构造函数和用户提供的析构函数,这将导致派生的 class C 可以具有平凡的复制构造函数,但不能像 [class.copy.ctor#11.2] and [class.dtor#8.2] 那样具有平凡的析构函数,它们是以下规则:

A copy/move constructor for class X is trivial if it is not user-provided and if:

  • the constructor selected to copy/move each direct base class subobject is trivial

A destructor is trivial if it is not user-provided and if:

  • all of the direct base classes of its class have trivial destructors

class C 并非所有这些限制都满足,因此在这种情况下无法创建临时对象。这意味着应该使用复制构造函数从参数初始化参数。这两个具有自动存储持续时间的对象将分别在块退出时销毁。也就是说,这里只应打印两次 ~B