编译器是否能够将dynamic_cast优化掉?

4

我接触过这样的代码:

dynamic_cast<A*>(p)->foo();

当 dynamic_cast 返回 0 时,会出现未定义的行为,这当然是很糟糕的。接着我想,它可能会带来一个可怕的意外,即由于当 p 可以强制转换成 A 类型指针时与执行 static_cast 相同,而当不能进行强制转换时就会产生未定义的行为,编译器可以将 dynamic_cast 转换为 static_cast 并保持符合规范的行为。话虽如此,我使用了编译器浏览器尝试了以下代码:
class A {
    virtual void bar() = 0;
};

class B final: public A {
    public:
    void foo();
    void bar() override;
};

void h();

void f(A* const p)
{
    dynamic_cast<B*>(p)->foo();
}

令我惊讶的是,每个编译器都保留了 dynamic_cast 调用。是否有什么我误解的地方,或者编译器根本没有进行可能的优化?


1
看起来你期望编译器的作者做类似这样的事情:https://stackoverflow.blog/2019/10/29/my-most-embarrassing-mistakes-as-a-programmer-so-far/。实际上并没有这样的必要(不过,如果能发出一个警告,说明动态转换功能的一部分未被使用,因此可以用静态转换替代,那就很好了)。 - Kit.
1
@Someprogrammerdude 那就是关键所在。在你描述的情况下,完整表达式具有未定义行为。编译器一直以这种方式利用 UB。但是目前看来,他们似乎不会利用这种场景。 - Lightness Races in Orbit
1
@ChrisMM 当程序具有未定义行为时。 - Lightness Races in Orbit
2
@ChrisMM:澄清一下,在C++中的未定义行为可以做任何事情,包括更改过去操作的结果。这使得编译器可以在这种特定情况下假装dynamic_cast没有返回空指针。 - MSalters
3
换句话说,编译器可以利用上述示例中程序只有一个情况是正确的这一事实,因此 dynamic_cast 不是必需的。你可能认为这是无关紧要或白日梦,但其实不是。这正是编译器一直在执行的那种优化。这也是 UB 的原因所在。 - Lightness Races in Orbit
显示剩余10条评论
1个回答

7
我认为优化是可行的。编译器可以看到只存在两种情况:
  • 操作数的动态类型是 "B",因此转换可以是静态的
  • 操作数的动态类型是从 "A" 派生出来的其他类型,在这种情况下结果是 "nullptr",同时完整表达式具有未定义行为。
(由于 final 说明符,不可能存在派生自 "B" 的任何类型。这很方便,因为如果存在这样的类型,则静态转换不一定是动态转换的适当替代品 - 必须考虑多重继承和侧投等情况,并且可能没有足够的信息在这个翻译单元中完成。链接时优化会进一步减轻这种情况,但实际上大多数像这样的优化都发生在编译时,并且支持像"dlopen"之类的东西的任何平台也将否决这种可能性。) 因此,我们只有一个产生良好定义程序的情况。
由于编译器允许假设输入不具有未定义的行为,如果删除所有导致 UB 的代码路径后,您只剩下一个可能的结果,那么编译器可以假设那总是是结果。这就是未定义行为存在的核心原因,以允许这样的优化,编译器一直在进行这些操作,以使您的代码更快。
我承认对于主流编译器不利用这种机会感到有点惊讶。至少我期望会有一个关于冗余 dynamic_cast 的警告;但仍然有可能某些静态分析工具会给出这个提示。

如果编译器在内部将这两种情况表示为两个分支,并且优化器在其中一个分支中发现了nullptr UB,那么我可以看到这种情况会“无意中”发生。通常情况下,一般情况更为普遍。 - MSalters
@MSalters 没错,这就是可以进行(并且经常进行)的优化。我承认有点惊讶,但无所谓。 - Lightness Races in Orbit
@Kit。好的,是的,我认为你是对的。我会调整一下。谢谢 :) - Lightness Races in Orbit

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