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.如果C是T指向或引用的类类型,则运行时检查在逻辑上执行如下:
(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& 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或整个程序的知识,你必须做在运行时检查(因为C和D可能看不到你).随着final上B,你应该能够逃脱不这样做,但我并不感到惊讶,如果编译器还没有得到解决的又优化这种情况.