最近我查看了一个开源项目的代码,我看到了一堆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 *
,那么编译器实际上要做的是向指针添加一些偏移量,偏移量仅取决于类型A
,B
的布局(以及潜在的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.如果C
是T
指向或引用的类类型,则运行时检查在逻辑上执行如下:
(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 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
:
似乎使用两个类的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)
一个合理的建议?
答案 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
或整个程序知识的世界中,您必须在运行时进行检查(因为您可能看不到final
和C
)。 D
final
上的B
,你应该可以逃脱,但如果编译器还没有优化那个案例,我也不会感到惊讶。