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

时间:2016-06-26 06:10:55

标签: c++ c++11 casting undefined-behavior dynamic-cast

最近我查看了一个开源项目的代码,我看到了一堆T & object = *dynamic_cast<T*>(ptr);形式的语句。

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

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

我问过一位关于irc的开发者,他说,他认为static_cast贬低是不安全的。他们可以添加一个断言,但即使他们没有,他说当实际使用obj时,你仍然会得到一个空指针取消引用并崩溃。 (因为失败时,dynamic_cast会将指针转换为null,然后当您访问任何成员时,您将读取非常接近零的某个值的地址,操作系统不会允许。 )如果你使用static_cast,它会变坏,你可能会得到一些内存损坏。因此,通过使用*dynamic_cast选项,您可以获得更快的速度以获得更好的可调试性。你不是要为断言买单,而是你基本上依靠操作系统去捕捉nullptr dereference,至少是我所理解的。

我当时接受了这个解释,但它让我感到困扰,我想了更多。

这是我的理由。

如果我理解标准权利,static_cast指针演员基本上意味着做一些固定的指针算术。也就是说,如果我有A * a,并且我将其静态转换为相关类型B *,那么编译器实际上要做的是向指针添加一些偏移量,偏移量仅取决于类型AB的布局(以及潜在的C ++实现)。这个理论可以通过静态转换指针void *来测试,并在静态转换之前和之后输出它们。我希望如果你看一下生成的程序集,static_cast将变为&#34;将一些固定常量添加到与指针对应的寄存器中。&#34;

dynamic_cast指针强制转换意味着,首先检查RTTI,如果有效则根据动态类型进行静态转换。如果不是,则返回nullptr。所以,我希望编译器在某些时候将dynamic_cast<B*>(ptr) ptr A*类型(__validate_dynamic_cast_A_to_B(ptr) ? static_cast<B*>(ptr) : nullptr) 扩展为类似

的表达式
*

但是,如果我们*表示dynamic_cast的结果,nullptr的{​​{1}}是UB,那么我们隐含地承诺nullptr分支永远不会发生。并且允许符合要求的编译器向后推理&#34;从那里消除空检查,这是Chris Lattner famous blog post的一个由家庭驱动的家庭。

如果测试函数__validate_dynamic_cast_A_to_B(ptr)对优化器不透明,即它可能有副作用,那么优化器就无法摆脱它,即使它&#34;知道&#34; nullptr分支没有发生。但是,这个函数可能对优化器不透明 - 可能它对其可能的副作用有很好的理解。

所以,我的期望是优化器基本上将*dynamic_cast<T*>(ptr)转换为*static_cast<T*>(ptr),并且交换它们应该给出相同的生成程序集。

如果是真的,这就证明了*dynamic_cast<T*>代码气味的原始论证是正确的,即使你在代码中并不真正关心UB而只关心什么&#34;实际上&#34 ;发生。因为,如果允许符合标准的编译器将其静默地更改为static_cast,那么您没有获得任何您认为的安全性,因此您应该明确static_cast或明确断言。至少,这将是我在代码审查中的投票。我试图弄清楚这个论点是否真的正确。

以下是标准对dynamic_cast的说法:

  

[5.2.7]动态广告[expr.dynamic.cast]
  1.表达式dynamic_cast<T>(v)的结果是将表达式v转换为类型T的结果。 T应该是指向完整类类型的指针或引用,或&#34;指向cv void的指针。&#34; dynamic_cast运算符不应丢弃constness 。
  ...
  8.如果CT指向或引用的类类型,则运行时检查在逻辑上执行如下:
  (8.1) - 如果在v指向(引用)的最派生对象中,v指向(引用)C对象的公共基类子对象,并且仅类型为C的一个对象派生自v指向(引用)到C对象的结果点(引用)的子对象。   (8.2) - 否则,如果v指向(引用)最派生对象的公共基类子对象,并且最派生对象的类型具有类型为C的基类,则是明确的和公开的,结果指​​向(引用)最派生对象的C子对象。
  (8.3) - 否则,运行时检查失败。

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

然而,在实际查看某些代码时,情节会有所增加。

这是我为调查此问题而制作的一个简单程序:

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    $8, %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    $8, %rsp
        ret
.L4:
        movl    12, %eax
        ud2
main:
        subq    $8, %rsp
        movl    $16, %edi
        call    operator new(unsigned long)
        movq    %rax, %rdi
        movl    $5, 8(%rax)
        movq    vtable for B+16, (%rax)
        movl    $10, 12(%rax)
        call    visit(A*)
        movl    $16, %edi
        call    operator new(unsigned long)
        movq    vtable for A+16, (%rax)
        movl    $10, 8(%rax)
        movq    %rax, %rdi
        call    visit(A*)
        movl    output(%rip), %eax
        addq    $8, %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    $8, %rsp
        movl    $16, %edi
        call    operator new(unsigned long)
        movl    $16, %edi
        subl    $20, 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    $8, %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    $8, %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    $16, %edi
        callq   operator new(unsigned long)
        movl    $5, 8(%rax)
        movq    vtable for B+16, (%rax)
        movl    $10, 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    $16, %edi
        callq   operator new(unsigned long)
        movq    vtable for A+16, (%rax)
        movl    $10, 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

似乎使用两个类的typeinfo生成对神秘__dynamic_cast函数的调用,无论如何。即使所有优化都已启用,B也标记为最终。

  • 这个低级别的通话是否有副作用,我没有考虑过?我的理解是vtable基本上是固定的,并且对象中的vptr不会改变......我是对的吗?我只是基本熟悉如何实际实现vtable,而且我通常在代码中避免使用虚函数,因此我并没有真正深入思考它或积累经验。

  • 我是否正确,符合标准的编译器可以*dynamic_cast<T*>(ptr)替换为*static_cast<T*>(ptr)作为有效优化?

  • &#34;通常&#34;是真的吗? (意思是,在x86机器上,让我们说,并且在通常&#34;复杂性的层次结构中的类之间进行转换)dynamic_cast无法优化,并且实际上 即使您nullptr之后立即生成*,导致nullptr取消引用并在访问对象时崩溃?

  • 是&#34;始终用*dynamic_cast<T*>(ptr) +测试或某种断言,或dynamic_cast&#34;替换*static_cast<T*>(ptr)一个合理的建议?

1 个答案:

答案 0 :(得分:9)

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的{​​{1}}基础,那么您实际上*可以获得指向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或整个程序知识的世界中,您必须在运行时进行检查(因为您可能看不到finalC)。 D final上的B,你应该可以逃脱,但如果编译器还没有优化那个案例,我也不会感到惊讶。