析构函数中的动态类型转换

8

这段代码是否合法?

class Base1 {
};

class Base2 {
public:
    virtual ~Base2() {
        if (!dynamic_cast<Base1*>(this))
            std::cout << "aaaa" << std::endl;
    }
    Base2() {
    }
};

class MyClass: public Base1, public Base2 {
public:
    MyClass() {
    }
    virtual ~MyClass() {
        std::cout << "bbb" << std::endl;
    }
};

int main() {
    MyClass s;
    return 0;
}

我看到了两个打印输出,但实际上只应该有一个。我猜测dynamic cast(动态类型转换)可能出现了错误。能否进行这种检查呢?


你能澄清一下你想要检查什么吗?Base2想知道它是否是一个派生类的基类,该派生类具有Base1吗? - jtbandes
是的,我想在Base2中检查“this”是否也是base1的子项。 - greywolf82
@formerlyknownas_463035818 MyClass是Base1和Base2的子类,因此当Base2的析构函数运行时,动态转换应该是可以的,因为"this"也是base1的子类,但也许我错了,这就是我问的原因。 - greywolf82
1
@ greywolf82 哦,抱歉我错过了 ! - 463035818_is_not_a_number
2
在构造函数和析构函数中,dynamic_cast的行为是否不同? - François Andrieux
显示剩余3条评论
3个回答

7
也许我自己找到了解决方案,答案是否定的:

cppreference.com文档的第6条中可以看到:

当在构造函数或析构函数(直接或间接)中使用dynamic_cast,并且表达式指向当前正在构建/销毁的对象时,该对象被认为是最派生的对象。 如果new-type不是指向构造函数/析构函数自身类或其基类之一的指针或引用,则行为未定义。

请参见标准文献[class.cdtor]/6。

由于我在Base2析构函数中将其转换为Base1,因此这种行为是未定义的。


1
抱歉,参考文献为[class.cdtor]/6,与5不同。在C++17(草案N4659)中是5,现在看起来是/6 - ChrisMM
@ChrisMM 那段话似乎没有提到这个答案中提到的未定义行为。实际上,我无法在标准中找到这个限制的任何地方。 - walnut
我认为cppreference在写"new-type"时应该是"expression"的错误。链接的标准段落确实说,如果它引用正在销毁的对象,则操作数需要具有析构函数类或其基类的(静态)类型。在问题的代码中,this是析构函数的类型,因此我不认为会出现UB。然而,同一标准段落确实说,正在销毁的对象的最派生类型将被视为析构函数的类,因此观察到了在其他答案中解释的行为。 - walnut
@walnut,我只是在发布标准中相关的段落,而不是我的答案。我同意它不是UB。 - ChrisMM

3
我同意@j6t的答案,但这里是一个扩展的解释,并带有标准参考。
C++17标准(最终草案)的[class.cdtor]/5描述了dynamic_cast对正在构建和销毁的对象的特殊行为,之前的标准版本也是如此。
特别地,它说:
当在析构函数中使用dynamic_cast时,如果dynamic_cast的操作数引用正在构建或销毁的对象,则此对象被视为具有[...]析构函数类的类型的最派生对象。如果dynamic_cast的操作数引用正在[...]销毁的对象,并且操作数的静态类型不是指向[...]析构函数自身类或其基类之一的指针或对象,则动态转换的结果未定义行为。
由于操作数是表达式this,它显然具有指向析构函数自身类的指针类型,因为它出现在析构函数中,因此未定义行为不适用于此处。
然而,第一句话说明dynamic_cast的行为就像*this是类型为Base2的最终派生对象,因此将*this强制转换为Base1永远不可能成功,因为Base2不是从Base1派生出来的,而且dynamic_cast<Base1*>(this)将始终返回一个空指针,导致您看到的行为。

cppreference.com指出,如果转换的目标类型不是析构函数所在类或其基类的类型,而不是操作数的类型,则会发生未定义行为。我认为这只是一个错误。可能在第6个要点中提到的“new-type”应该改为“expression”,这将使它与我上面的解释相匹配。


2
dynamic_cast 在这种情况下是定义良好的。你观察到两行输出都是正确的。
你错误地认为在 Base2 的析构函数中,this 是一个派生类。此时,派生类部分已经被销毁,因此它不能再是派生类了。实际上,在运行 Base2 析构函数时,this 指向的对象只是一个 Base2 对象。由于 Base2Base1 没有任何关系,因此 dynamic_cast 返回一个空指针,条件被相应地执行。
编辑: 标准 表示:

当在构造函数[...]或析构函数[...]中使用 dynamic_­cast 时,如果 dynamic_­cast 的操作数引用正在构建或销毁的对象,则认为该对象是具有构造函数或析构函数类的类型的最派生对象。如果 dynamic_­cast 的操作数引用正在构建或销毁的对象,并且操作数的静态类型不是指向构造函数或析构函数自己的类或其基类之一的指针或对象,则 dynamic_­cast 的结果是未定义的行为。

操作数 this 引用正在销毁的对象。因此,析构函数的类 (Base2) 被认为是最派生类,这就是为什么该对象与目标类型 (Base1*) 没有任何关系的原因。此外,操作数 this 的静态类型是 Base2* const,显然是指向析构函数自己的类的指针。因此,未定义行为的规则不适用。总之,我们有良好定义的行为。

1
这与其他回答相矛盾,其他回答引用标准指出该代码具有未定义的行为。如果要反对这一点,您需要一些好的论据,仅仅说不够。 - 463035818_is_not_a_number
1
@formerlyknownas_463035818 另一个答案引用了cppreference而不是标准。这可能不太清楚。它提到的标准段似乎没有像cppreference那样说。 - walnut
@walnut,我原本以为这是标准中的引用,后来发现它被编辑成cppref的引用。总之,这些答案互相矛盾,其中一个有支持它的来源,而另一个则没有,仅此而已... - 463035818_is_not_a_number
@walnut 我自己检查了一下,另一个答案中提到的[class.cdtor]/6与cppref中的内容相同:“如果dynamic_cast的操作数引用正在构造或销毁的对象,并且操作数的静态类型不是指向构造函数或析构函数自身类或其基类之一的指针或对象,则dynamic_cast的结果为未定义行为。” - 463035818_is_not_a_number
1
我已经添加了对标准的解释。 - j6t
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接