`*dynamic_cast<T*>(...)` 是什么意思?

What is the meaning of `*dynamic_cast<T*>(...)`?

最近在看一个开源项目的代码,看到一堆T & object = *dynamic_cast<T*>(ptr);.

形式的语句

(实际上这发生在用于声明许多遵循类似模式的函数的宏中。)

对我来说,这看起来像是代码的味道。我的推理是,如果您 知道 转换会成功,那么为什么不使用 static_cast?如果您不确定,那么您不应该使用断言来测试吗?由于编译器可以假定您 * 的任何指针都不为空。

我在 irc 上问了一位开发者,他说,他认为 static_cast 沮丧是不安全的。他们可以添加一个断言,但即使他们不这样做,他说您仍然会在实际使用 obj 时获得空指针取消引用和崩溃。 (因为,失败时,dynamic_cast 会将指针转换为 null,然后当您访问任何成员时,您将从某个值非常接近零的地址读取,而 OS 不会允许。)如果你使用 static_cast,它变坏了,你可能会得到一些内存损坏。因此,通过使用 *dynamic_cast 选项,您可以牺牲速度来换取更好的可调试性。您没有为断言付费,而是基本上依靠 OS 来捕获 nullptr 取消引用,至少我是这么理解的。

当时我接受了这个解释,但它困扰着我,我又想了想。

这是我的推理。

如果我对标准的理解正确,static_cast 指针转换基本上意味着进行一些固定指针运算。也就是说,如果我有 A * a,并且我将它静态转换为相关类型 B *,编译器实际上要做的是向指针添加一些偏移量,偏移量仅取决于AB 类型的布局(以及可能的 C++ 实现)。这个理论可以通过静态转换指向 void * 的指针并在静态转换之前和之后输出它们来测试。我希望如果您查看生成的程序集,static_cast 会变成 "add some fixed constant to the register corresponding to the pointer."

A dynamic_cast pointer cast 的意思是,首先检查 RTTI,只有在基于动态类型有效的情况下才进行静态转换。如果不是,则 return nullptr。所以,我希望编译器在某个时候将 dynamic_cast<B*>(ptr) 的表达式 ptr 的类型 A* 扩展为

这样的表达式
(__validate_dynamic_cast_A_to_B(ptr) ? static_cast<B*>(ptr) : nullptr)

然而,如果我们然后*dynamic_cast的结果,nullptr*是UB,所以我们隐含地承诺nullptr 分支永远不会发生。并且允许符合规范的编译器 "reason backwards" 并消除空检查,这是 Chris Lattner 的 famous blog post.

中的一个重点

如果测试函数 __validate_dynamic_cast_A_to_B(ptr) 对优化器来说是不透明的,即它可能有副作用,那么优化器无法摆脱它,即使它 "knows" nullptr 分支没有发生。然而,可能这个函数对优化器来说不是不透明的——可能它对其可能的副作用有很好的理解。

因此,我的期望是优化器基本上会将 *dynamic_cast<T*>(ptr) 转换为 *static_cast<T*>(ptr),并且交换这些应该会生成相同的程序集。

如果为真,那将证明我最初的论点 *dynamic_cast<T*> 是一种代码味道,即使您并不真正关心代码中的 UB 并且只关心 "actually" 发生了什么。因为,如果允许符合规范的编译器将其静默更改为 static_cast,那么您将无法获得您认为的任何安全性,因此您应该显式 static_cast 或显式断言。至少,那将是我在代码审查中的投票。我想弄清楚这个论点是否真的正确。


以下是标准对 dynamic_cast 的描述:

[5.2.7] Dynamic Cast [expr.dynamic.cast]
1. The result of the expression dynamic_cast<T>(v) is the result of converting the expression v to type T. T shall be a pointer or reference to a complete class type, or "pointer to cv void." The dynamic_cast operator shall not cast away constness.
...
8. If C is the class type to which T points or refers, the run-time check logically executes as follows:
(8.1) - If, in the most derived object pointed (referred) to by v, v points (refers) to a public base class subobject of a C object, and if only one object of type C is derived from the subobject pointed (referred) to by v the result points (refers) to that C object.
(8.2) - Otherwise, if v points (refers) to a public base class subobject of the most derived object, and the type of the most derived object has a base class, of type C, that is unambiguous and public, the result points (refers) to the C subobject of the most derived object.
(8.3) - Otherwise, the run-time check fails.

假设 类 的层次结构在编译时已知,那么每个 类 在彼此布局中的相对偏移量也是已知的。如果 v 是指向类型 A 的指针,并且我们想将其转换为类型 B 的指针,并且转换是明确的,那么 v 必须进行移位take 是一个编译时常量。即使 v 实际上指向一个更派生类型的对象 C,这个事实也不会改变 A 子对象相对于 B 子对象的位置,对吧?因此,无论 C 是什么类型,即使它是来自另一个编译单元的某种未知类型,据我所知 dynamic_cast<T*>(ptr) 的结果也只有两个可能的值,nullptr 或 "fixed-offset from ptr".


但是,在实际查看某些代码生成时,情节会变得有些复杂。

这是我用来研究这个问题的一个简单程序:


int output = 0;

struct A {
  explicit A(int n) : num_(n) {}
  int num_;

  virtual void foo() {
    output += num_;
  }
};

struct B final : public A {
  explicit B(int n) : A(n), num2_(2 * n) {}

  int num2_;

  virtual void foo() override {
    output -= num2_;
  }
};

void visit(A * ptr) {
  B & b = *dynamic_cast<B*>(ptr);
  b.foo();
  b.foo();
}

int main() {
  A * ptr = new B(5); 

  visit(ptr);

  ptr = new A(10);
  visit(ptr);

  return output;
}

根据 godbolt compiler explorergcc 5.3 x86 程序集,带有选项 -O3 -std=c++11,如下所示:


A::foo():
        movl    8(%rdi), %eax
        addl    %eax, output(%rip)
        ret
B::foo():
        movl    12(%rdi), %eax
        subl    %eax, output(%rip)
        ret
visit(A*):
        testq   %rdi, %rdi
        je      .L4
        subq    , %rsp
        xorl    %ecx, %ecx
        movl    typeinfo for B, %edx
        movl    typeinfo for A, %esi
        call    __dynamic_cast
        movl    12(%rax), %eax
        addl    %eax, %eax
        subl    %eax, output(%rip)
        addq    , %rsp
        ret
.L4:
        movl    12, %eax
        ud2
main:
        subq    , %rsp
        movl    , %edi
        call    operator new(unsigned long)
        movq    %rax, %rdi
        movl    , 8(%rax)
        movq    vtable for B+16, (%rax)
        movl    , 12(%rax)
        call    visit(A*)
        movl    , %edi
        call    operator new(unsigned long)
        movq    vtable for A+16, (%rax)
        movl    , 8(%rax)
        movq    %rax, %rdi
        call    visit(A*)
        movl    output(%rip), %eax
        addq    , %rsp
        ret
typeinfo name for A:
typeinfo for A:
typeinfo name for B:
typeinfo for B:
vtable for A:
vtable for B:
output:
        .zero   4

当我将 dynamic_cast 更改为 static_cast 时,我得到以下内容:


A::foo():
        movl    8(%rdi), %eax
        addl    %eax, output(%rip)
        ret
B::foo():
        movl    12(%rdi), %eax
        subl    %eax, output(%rip)
        ret
visit(A*):
        movl    12(%rdi), %eax
        addl    %eax, %eax
        subl    %eax, output(%rip)
        ret
main:
        subq    , %rsp
        movl    , %edi
        call    operator new(unsigned long)
        movl    , %edi
        subl    , output(%rip)
        call    operator new(unsigned long)
        movl    12(%rax), %edx
        movl    output(%rip), %eax
        subl    %edx, %eax
        subl    %edx, %eax
        movl    %eax, output(%rip)
        addq    , %rsp
        ret
output:
        .zero   4

此处与 clang 3.8 相同,选项相同。

dynamic_cast:


visit(A*):                            # @visit(A*)
        xorl    %eax, %eax
        testq   %rdi, %rdi
        je      .LBB0_2
        pushq   %rax
        movl    typeinfo for A, %esi
        movl    typeinfo for B, %edx
        xorl    %ecx, %ecx
        callq   __dynamic_cast
        addq    , %rsp
.LBB0_2:
        movl    output(%rip), %ecx
        subl    12(%rax), %ecx
        movl    %ecx, output(%rip)
        subl    12(%rax), %ecx
        movl    %ecx, output(%rip)
        retq

B::foo():                            # @B::foo()
        movl    12(%rdi), %eax
        subl    %eax, output(%rip)
        retq

main:                                   # @main
        pushq   %rbx
        movl    , %edi
        callq   operator new(unsigned long)
        movl    , 8(%rax)
        movq    vtable for B+16, (%rax)
        movl    , 12(%rax)
        movl    typeinfo for A, %esi
        movl    typeinfo for B, %edx
        xorl    %ecx, %ecx
        movq    %rax, %rdi
        callq   __dynamic_cast
        movl    output(%rip), %ebx
        subl    12(%rax), %ebx
        movl    %ebx, output(%rip)
        subl    12(%rax), %ebx
        movl    %ebx, output(%rip)
        movl    , %edi
        callq   operator new(unsigned long)
        movq    vtable for A+16, (%rax)
        movl    , 8(%rax)
        movl    typeinfo for A, %esi
        movl    typeinfo for B, %edx
        xorl    %ecx, %ecx
        movq    %rax, %rdi
        callq   __dynamic_cast
        subl    12(%rax), %ebx
        movl    %ebx, output(%rip)
        subl    12(%rax), %ebx
        movl    %ebx, output(%rip)
        movl    %ebx, %eax
        popq    %rbx
        retq

A::foo():                            # @A::foo()
        movl    8(%rdi), %eax
        addl    %eax, output(%rip)
        retq

output:
        .long   0                       # 0x0

typeinfo name for A:

typeinfo for A:

typeinfo name for B:

typeinfo for B:

vtable for B:

vtable for A:

static_cast:


visit(A*):                            # @visit(A*)
        movl    output(%rip), %eax
        subl    12(%rdi), %eax
        movl    %eax, output(%rip)
        subl    12(%rdi), %eax
        movl    %eax, output(%rip)
        retq

main:                                   # @main
        retq

output:
        .long   0                       # 0x0

因此,在这两种情况下,似乎 dynamic_cast 都无法被优化器消除:

它似乎生成了对一个神秘的 __dynamic_cast 函数的调用,无论如何都使用了 类 的类型信息。即使所有优化都已启用,并且 B 标记为最终。

T& object = *dynamic_cast<T*>(ptr); 已损坏,因为它会在失败时调用 UB,句号。我认为没有必要强调这一点。即使它似乎适用于当前的编译器,它也可能不适用于具有更积极优化器的更高版本。

如果您想要检查并且不想被打扰编写断言,请使用在失败时抛出 bad_cast 的参考形式:

T& object = dynamic_cast<T&>(*ptr);

dynamic_cast 不仅仅是一次 运行 时间检查。它可以做 static_cast 做不到的事情。比如可以侧身施法

A   A (*)
|   |
B   C
\   /
 \ /
  D

如果实际的最派生对象是 D,并且您有一个指向标有 *A 基的指针,您实际上可以 dynamic_cast 它到获取指向 B 子对象的指针:

struct A { virtual ~A() = default; };
struct B : A {};
struct C : A {};
struct D : B, C {};
void f() {
    D d;
    C& c = d;
    A& a = c;
    assert(dynamic_cast<B*>(&a) != nullptr);
}

注意这里的static_cast是完全错误的。

(另一个突出的例子是 dynamic_cast 可以做一些 static_cast 做不到的事情,当你从一个虚拟基础转换到派生的 class 时。)

在没有final或全程序知识的世界中,您必须在运行时间进行检查(因为CD可能不可见给你)。在 B 上使用 final,你应该可以不这样做,但如果编译器还没有开始优化这种情况,我并不感到惊讶。