c++ “* dynamic_cast(…)”的含义是什么?

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

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

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

我问了irc上的一个开发者,他说,他认为static_cast downcast是不安全的.他们可以添加一个断言,但即使不这样做,他表示,当obj实际使用时,仍然会得到一个空指针解引用和崩溃. (因为,在失败的时候,dynamic_cast会将指针转换为null,那么当你访问任何成员时,你将会读取一些值非常接近零的地址,这个操作系统将不允许.)如果你使用static_cast ,它变坏了,你可能只是得到一些内存损坏.所以通过使用* dynamic_cast选项,你的速度要稍微好一点的可调试性.你不支付主张,而是基本上依靠操作系统来捕获nullptr的引用,至少这就是我所理解的.

我当时接受了这个解释,但是让我烦恼了,我再想一想.

这是我的推理.

如果我理解标准权限,static_cast指针的转换基本上意味着要做一些固定的指针算术.也就是说,如果我有A * a,我将静态转换为相关类型B *,编译器实际要做的是向指针添加一些偏移量,偏移仅取决于类型的布局A,B(和C可能执行).这个理论可以通过静态投射指针void *来进行测试,并在static cast之前和之后输出它们.我期望如果你看看生成的程序集,static_cast将变成“添加一些固定常量到与指针对应的寄存器”.

一个dynamic_cast指针转换意味着,首先检查RTTI,并且只有当静态转换根据动态类型有效时才执行静态转换.如果不是,则返回nullptr.所以,我希望编译器在某种程度上会将一个类型为A *的ptr扩展到一个表达式dynamic_cast< B *>(ptr)

(__validate_dynamic_cast_A_to_B(ptr) ? static_cast<B*>(ptr) : nullptr)

然而,如果我们*的dynamic_cast的结果,*的nullptr是UB,所以我们隐式地承诺,nullptr分支永远不会发生.而符合条件的编译器则被允许从此“倒退”,并消除零点检查,这是Chris Lattner的famous blog post的一个点.

如果测试函数__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] 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必须执行的移位是编译时常量.即使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 explorer,gcc 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不能被优化器消除:

似乎生成一个神秘的__dynamic_cast函数的调用,使用这两个类的typeinfo,无论什么.即使所有优化都处于开启状态,B标记为最终.

>这个低级别的电话有没有考虑的副作用?我的理解是,vtables基本上是固定的,一个对象中的vptr不会改变…我是对的吗?我只是基本熟悉vtables如何实际实现,而且我通常在我的代码中避免虚拟函数,所以我没有深入思考或积累的经验.
我正确的是,符合一致的编译器可以用* static_cast< T *>(ptr)替换* dynamic_cast< T *>(ptr)作为有效的优化?
>“通常”(意思是说,在x86机器上,就是说,在“通常”复杂度的层次结构中的类之间进行转换),一个dynamic_cast无法被优化,实际上会产生一个nullptr,即使你*紧随其后,导致在访问对象时无效的引用和崩溃?
>是否始终用dynamic_cast测试或断言取代* dynamic_cast< T *>(ptr),或者使用* static_cast< T *>(ptr)“声音建议?

夯; 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不能是从虚拟基础到派生类的转换.)

在没有最终或全程知识的世界中,您必须在运行时进行检查(因为C和D可能不可见).在B的最后,你应该能够摆脱不做,但如果编译器还没有得到优化,我并不感到惊讶.

翻译自:https://stackoverflow.com/questions/38035722/what-is-the-meaning-of-dynamic-castt

转载注明原文:c++ “* dynamic_cast(…)”的含义是什么?