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

Chr*_*eck 12 c++ dynamic-cast casting undefined-behavior c++11

最近我查看了一个开源项目的代码,我看到了一堆表单的声明T & object = *dynamic_cast<T*>(ptr);.

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

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

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

我当时接受了那个解释,但它让我感到困扰,我又想了一些.

这是我的推理.

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

一个dynamic_cast指针强制手段,首先检查RTTI和只做静态浇铸如果基于动态类型是有效的.如果不是,那就回来吧nullptr.于是,我想到的是,编译器会在某个点扩大为一个函数dynamic_cast<B*>(ptr),其中ptr的类型是A*到像表达

(__validate_dynamic_cast_A_to_B(ptr) ? static_cast<B*>(ptr) : nullptr)
Run Code Online (Sandbox Code Playgroud)

但是,如果我们再*在dynamic_cast的结果,*nullptr是UB,因此我们看好隐含的nullptr分支从未发生过.并且合规编译器被允许从中"向后推"并消除空检查,这是克里斯拉特纳着名的博客文章中的一个点驱动的家.

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

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

如果属实,这将证明我原来的说法,*dynamic_cast<T*>是一种代码味道,即使你在你的代码并不真正关心UB只在乎什么"实际"发生.因为,如果一个符合标准的编译将被允许将其更改为一个static_cast默默的,那么你没有得到你认为你是任何安全,所以你应该明确地static_cast或明确地断言.至少,这将是我在代码审查中的投票.我想弄清楚这个论点是否真的正确.


以下是标准所说的内容dynamic_cast:

[5.2.7]动态转换 [expr.dynamic.cast]
1.表达式dynamic_cast<T>(v)的结果是将表达式转换v为类型的结果T.T应该是指向完整类类型的指针或引用,或者是"指向cv void的指针".dynamic_cast经营者不得抛弃常量性.
...
8.如果CT指向或引用的类类型,则运行时检查在逻辑上执行如下:
(8.1) - 如果,在指向(引用)的最多派生对象中v,v指向(引用)a对象的公共基类子C对象,并且如果只有一个对象类型C是从该对象v的结果点(引用)指向(引用)的子对象派生的C.
(8.2) - 否则,如果v点(引用)到最派生对象的公共基类子对象,并且最派生对象的类型具有类型的基类C,即明确且公开的,则结果点(引用) )到C最派生对象的子对象.
(8.3) - 否则,运行时检查失败.

假设在编译时已知类的层次结构,则每个布局中的每个类的相对偏移量也是已知的.如果v是键入一个指针A,我们希望把它转换为类型的指针B,和演员是明确的,那转变v必须是一个编译时间常数.即使v实际指向更多派生类型的对象C,该事实也不会改变A子对象相对于B子对象的位置,对吧?所以,无论什么类型C的,即使它是从另一个编译单元一些未知类型的,据我所知的结果dynamic_cast<T*>(ptr)只有两个可能的值,nullptr或"从固定偏移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;
}
Run Code Online (Sandbox Code Playgroud)

根据godbolt编译器资源管理器,gcc 5.3x86程序集为此,带有选项-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
Run Code Online (Sandbox Code Playgroud)

当我dynamic_cast改为a时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
Run Code Online (Sandbox Code Playgroud)

这里有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:
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • 难道"通常"(意思是,在x86机器上,并且在"通常"复杂性的层次结构中的类之间进行转换)是真的dynamic_cast无法优化掉,并且实际上会产生一个nullptr即使你*正好在它之后,领先要nullptr取消引用和崩溃在访问对象?

  • "总是代替*dynamic_cast<T*>(ptr)与任何dynamic_cast某种,或用+测试或断言*static_cast<T*>(ptr)一个中肯的意见"?

T.C*_*.C. 9

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

如果你想要检查并且不想打扰写一个断言,请使用引发bad_cast失败的引用表单:

T& object = dynamic_cast<T&>(*ptr);
Run Code Online (Sandbox Code Playgroud)

dynamic_cast不仅仅是运行时检查.它可以做的事情static_cast不可能.例如,它可以横向投射.

A   A (*)
|   |
B   C
\   /
 \ /
  D
Run Code Online (Sandbox Code Playgroud)

如果实际的派生对象是a 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);
}
Run Code Online (Sandbox Code Playgroud)

请注意,static_cast这里完全错了.

(另一个dynamic_cast可以做某事的突出例子static_cast是当你从虚拟基类转换为派生类时.)

在没有世界final或整个程序的知识,你必须做在运行时检查(因为CD可能看不到你).随着finalB,你应该能够逃脱不这样做,但我并不感到惊讶,如果编译器还没有得到解决的又优化这种情况.