【问题标题】:Destructor called more times than it should. Bug in MSVC or not?析构函数调用的次数超过了它应该调用的次数。 MSVC 中的错误与否?
【发布时间】:2021-05-31 21:49:04
【问题描述】:

这个程序:

#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,即3次调用析构函数,为什么?

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

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

这是 MSVC 编译器中的错误吗?或者这是正确的行为?

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

【问题讨论】:

  • 它看起来确实像一个错误。可能它正在为“派生到基础的转换”创建一个临时对象,然后将其复制到参数对象。但我认为即使在 C++17 使事情变得更严格之前也不允许这样做,因为通常参数对象是从表达式初始化的。可平凡复制的类型可能有一些角落规则,但具有用户定义析构函数的类不是可平凡复制的。

标签: c++ visual-c++ language-lawyer


【解决方案1】:

如果您打印地址:

#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 次。似乎有一个临时的参与,据我所知,这是不允许的。这让我相信这是一个错误。这仅在复制构造函数很简单时才会发生。由于析构函数不是微不足道的,它是可观察的行为,不包含在 as-if 规则中。

【讨论】:

    【解决方案2】:

    在禁用优化的情况下查看 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 是无操作的。看起来两者都适合没有成员的班级;我们添加成员,我们将看到将 C 切片到 b 和 B 的复制。

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

    【讨论】:

    • 为什么T2没有被复制就没有通过?
    • 我不知道,我根据我的猜测解释了编译器结果,但无法推理。
    【解决方案3】:

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

    当类类型 X 的对象被传递给函数或从函数返回时,如果 X 至少有一个符合条件的复制或移动构造函数 ([special]),每个这样的构造函数都是微不足道的,并且X 的析构函数要么微不足道,要么被删除,允许实现创建一个临时对象来保存函数参数或结果对象。临时对象分别由函数参数或返回值构造,并且函数的参数或返回对象被初始化,就好像使用合格的平凡构造函数复制临时对象(即使该构造函数不可访问或不会被重载决议选择来执行对象的复制或移动)。

    在您的示例中,传递的参数类型为C,它派生自基类B,该基类具有非用户提供的复制构造函数和用户提供的析构函数,这将导致派生根据[class.copy.ctor#11.2] 和[class.dtor#8.2],C 类可以有一个普通的复制构造函数,但不能有普通的析构函数,它们是以下规则:

    类 X 的复制/移动构造函数是微不足道的,如果它不是用户提供的并且如果:

    • 选择复制/移动每个直接基类子对象的构造函数是微不足道的

    如果析构函数不是用户提供的并且满足以下条件,则它是微不足道的:

    • 其类的所有直接基类都有微不足道的析构函数

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-03
      • 2012-08-06
      • 2010-12-29
      • 2014-06-06
      • 1970-01-01
      相关资源
      最近更新 更多