`*dynamic_cast<T*>(...)` 是什么意思?
What is the meaning of `*dynamic_cast<T*>(...)`?
最近在看一个开源项目的代码,看到一堆T & object = *dynamic_cast<T*>(ptr);
.
形式的语句
(实际上这发生在用于声明许多遵循类似模式的函数的宏中。)
对我来说,这看起来像是代码的味道。我的推理是,如果您 知道 转换会成功,那么为什么不使用 static_cast
?如果您不确定,那么您不应该使用断言来测试吗?由于编译器可以假定您 *
的任何指针都不为空。
我在 irc 上问了一位开发者,他说,他认为 static_cast
沮丧是不安全的。他们可以添加一个断言,但即使他们不这样做,他说您仍然会在实际使用 obj
时获得空指针取消引用和崩溃。 (因为,失败时,dynamic_cast
会将指针转换为 null,然后当您访问任何成员时,您将从某个值非常接近零的地址读取,而 OS 不会允许。)如果你使用 static_cast
,它变坏了,你可能会得到一些内存损坏。因此,通过使用 *dynamic_cast
选项,您可以牺牲速度来换取更好的可调试性。您没有为断言付费,而是基本上依靠 OS 来捕获 nullptr 取消引用,至少我是这么理解的。
当时我接受了这个解释,但它困扰着我,我又想了想。
这是我的推理。
如果我对标准的理解正确,static_cast
指针转换基本上意味着进行一些固定指针运算。也就是说,如果我有 A * a
,并且我将它静态转换为相关类型 B *
,编译器实际上要做的是向指针添加一些偏移量,偏移量仅取决于A
、B
类型的布局(以及可能的 C++ 实现)。这个理论可以通过静态转换指向 void *
的指针并在静态转换之前和之后输出它们来测试。我希望如果您查看生成的程序集,static_cast
会变成 "add some fixed constant to the register corresponding to the pointer."
A dynamic_cast
pointer cast 的意思是,首先检查 RTTI,只有在基于动态类型有效的情况下才进行静态转换。如果不是,则 return nullptr
。所以,我希望编译器在某个时候将 dynamic_cast<B*>(ptr)
的表达式 ptr
的类型 A*
扩展为
这样的表达式
(__validate_dynamic_cast_A_to_B(ptr) ? static_cast<B*>(ptr) : nullptr)
然而,如果我们然后*
dynamic_cast的结果,nullptr
的*
是UB,所以我们隐含地承诺nullptr
分支永远不会发生。并且允许符合规范的编译器 "reason backwards" 并消除空检查,这是 Chris Lattner 的 famous blog post.
中的一个重点
如果测试函数 __validate_dynamic_cast_A_to_B(ptr)
对优化器来说是不透明的,即它可能有副作用,那么优化器无法摆脱它,即使它 "knows" nullptr 分支没有发生。然而,可能这个函数对优化器来说不是不透明的——可能它对其可能的副作用有很好的理解。
因此,我的期望是优化器基本上会将 *dynamic_cast<T*>(ptr)
转换为 *static_cast<T*>(ptr)
,并且交换这些应该会生成相同的程序集。
如果为真,那将证明我最初的论点 *dynamic_cast<T*>
是一种代码味道,即使您并不真正关心代码中的 UB 并且只关心 "actually" 发生了什么。因为,如果允许符合规范的编译器将其静默更改为 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
必须进行移位take 是一个编译时常量。即使 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 , %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 , %rsp
ret
.L4:
movl 12, %eax
ud2
main:
subq , %rsp
movl , %edi
call operator new(unsigned long)
movq %rax, %rdi
movl , 8(%rax)
movq vtable for B+16, (%rax)
movl , 12(%rax)
call visit(A*)
movl , %edi
call operator new(unsigned long)
movq vtable for A+16, (%rax)
movl , 8(%rax)
movq %rax, %rdi
call visit(A*)
movl output(%rip), %eax
addq , %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 , %rsp
movl , %edi
call operator new(unsigned long)
movl , %edi
subl , 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 , %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 , %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 , %edi
callq operator new(unsigned long)
movl , 8(%rax)
movq vtable for B+16, (%rax)
movl , 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 , %edi
callq operator new(unsigned long)
movq vtable for A+16, (%rax)
movl , 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
函数的调用,无论如何都使用了 类 的类型信息。即使所有优化都已启用,并且 B
标记为最终。
这个底层调用是不是有我没有考虑到的副作用?我的理解是 vtables 基本上是固定的,对象中的 vptr 不会改变......我是对的吗?我对 vtables 的实际实现方式只有基本的了解,而且我通常在代码中避免虚函数,所以我没有真正深入思考或积累经验。
符合标准的编译器 可以 将 *dynamic_cast<T*>(ptr)
替换为 *static_cast<T*>(ptr)
作为有效优化,我说得对吗?
"usually" 是真的吗(意思是,在 x86 机器上,比方说,在 "usual" 复杂度的层次结构中 类 之间进行转换)一个 dynamic_cast
不能被优化掉,并且 实际上 会产生一个 nullptr
即使你 *
它紧随其后,导致 nullptr
取消引用并在访问对象时崩溃?
"always replace *dynamic_cast<T*>(ptr)
with either dynamic_cast
+ test or assertion of some kind, or with *static_cast<T*>(ptr)
" 是一个合理的建议吗?
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
基的指针,您实际上可以 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
做不到的事情,当你从一个虚拟基础转换到派生的 class 时。)
在没有final
或全程序知识的世界中,您必须在运行时间进行检查(因为C
和D
可能不可见给你)。在 B
上使用 final
,你应该可以不这样做,但如果编译器还没有开始优化这种情况,我并不感到惊讶。
最近在看一个开源项目的代码,看到一堆T & object = *dynamic_cast<T*>(ptr);
.
(实际上这发生在用于声明许多遵循类似模式的函数的宏中。)
对我来说,这看起来像是代码的味道。我的推理是,如果您 知道 转换会成功,那么为什么不使用 static_cast
?如果您不确定,那么您不应该使用断言来测试吗?由于编译器可以假定您 *
的任何指针都不为空。
我在 irc 上问了一位开发者,他说,他认为 static_cast
沮丧是不安全的。他们可以添加一个断言,但即使他们不这样做,他说您仍然会在实际使用 obj
时获得空指针取消引用和崩溃。 (因为,失败时,dynamic_cast
会将指针转换为 null,然后当您访问任何成员时,您将从某个值非常接近零的地址读取,而 OS 不会允许。)如果你使用 static_cast
,它变坏了,你可能会得到一些内存损坏。因此,通过使用 *dynamic_cast
选项,您可以牺牲速度来换取更好的可调试性。您没有为断言付费,而是基本上依靠 OS 来捕获 nullptr 取消引用,至少我是这么理解的。
当时我接受了这个解释,但它困扰着我,我又想了想。
这是我的推理。
如果我对标准的理解正确,static_cast
指针转换基本上意味着进行一些固定指针运算。也就是说,如果我有 A * a
,并且我将它静态转换为相关类型 B *
,编译器实际上要做的是向指针添加一些偏移量,偏移量仅取决于A
、B
类型的布局(以及可能的 C++ 实现)。这个理论可以通过静态转换指向 void *
的指针并在静态转换之前和之后输出它们来测试。我希望如果您查看生成的程序集,static_cast
会变成 "add some fixed constant to the register corresponding to the pointer."
A dynamic_cast
pointer cast 的意思是,首先检查 RTTI,只有在基于动态类型有效的情况下才进行静态转换。如果不是,则 return nullptr
。所以,我希望编译器在某个时候将 dynamic_cast<B*>(ptr)
的表达式 ptr
的类型 A*
扩展为
(__validate_dynamic_cast_A_to_B(ptr) ? static_cast<B*>(ptr) : nullptr)
然而,如果我们然后*
dynamic_cast的结果,nullptr
的*
是UB,所以我们隐含地承诺nullptr
分支永远不会发生。并且允许符合规范的编译器 "reason backwards" 并消除空检查,这是 Chris Lattner 的 famous blog post.
如果测试函数 __validate_dynamic_cast_A_to_B(ptr)
对优化器来说是不透明的,即它可能有副作用,那么优化器无法摆脱它,即使它 "knows" nullptr 分支没有发生。然而,可能这个函数对优化器来说不是不透明的——可能它对其可能的副作用有很好的理解。
因此,我的期望是优化器基本上会将 *dynamic_cast<T*>(ptr)
转换为 *static_cast<T*>(ptr)
,并且交换这些应该会生成相同的程序集。
如果为真,那将证明我最初的论点 *dynamic_cast<T*>
是一种代码味道,即使您并不真正关心代码中的 UB 并且只关心 "actually" 发生了什么。因为,如果允许符合规范的编译器将其静默更改为 static_cast
,那么您将无法获得您认为的任何安全性,因此您应该显式 static_cast
或显式断言。至少,那将是我在代码审查中的投票。我想弄清楚这个论点是否真的正确。
以下是标准对 dynamic_cast
的描述:
[5.2.7]
Dynamic Cast[expr.dynamic.cast]
1. The result of the expressiondynamic_cast<T>(v)
is the result of converting the expressionv
to typeT
.T
shall be a pointer or reference to a complete class type, or "pointer to cv void." Thedynamic_cast
operator shall not cast away constness.
...
8. IfC
is the class type to whichT
points or refers, the run-time check logically executes as follows:
(8.1) - If, in the most derived object pointed (referred) to byv
,v
points (refers) to a public base class subobject of aC
object, and if only one object of typeC
is derived from the subobject pointed (referred) to byv
the result points (refers) to thatC
object.
(8.2) - Otherwise, ifv
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 typeC
, that is unambiguous and public, the result points (refers) to theC
subobject of the most derived object.
(8.3) - Otherwise, the run-time check fails.
假设 类 的层次结构在编译时已知,那么每个 类 在彼此布局中的相对偏移量也是已知的。如果 v
是指向类型 A
的指针,并且我们想将其转换为类型 B
的指针,并且转换是明确的,那么 v
必须进行移位take 是一个编译时常量。即使 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 , %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 , %rsp
ret
.L4:
movl 12, %eax
ud2
main:
subq , %rsp
movl , %edi
call operator new(unsigned long)
movq %rax, %rdi
movl , 8(%rax)
movq vtable for B+16, (%rax)
movl , 12(%rax)
call visit(A*)
movl , %edi
call operator new(unsigned long)
movq vtable for A+16, (%rax)
movl , 8(%rax)
movq %rax, %rdi
call visit(A*)
movl output(%rip), %eax
addq , %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 , %rsp
movl , %edi
call operator new(unsigned long)
movl , %edi
subl , 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 , %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 , %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 , %edi
callq operator new(unsigned long)
movl , 8(%rax)
movq vtable for B+16, (%rax)
movl , 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 , %edi
callq operator new(unsigned long)
movq vtable for A+16, (%rax)
movl , 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
函数的调用,无论如何都使用了 类 的类型信息。即使所有优化都已启用,并且 B
标记为最终。
这个底层调用是不是有我没有考虑到的副作用?我的理解是 vtables 基本上是固定的,对象中的 vptr 不会改变......我是对的吗?我对 vtables 的实际实现方式只有基本的了解,而且我通常在代码中避免虚函数,所以我没有真正深入思考或积累经验。
符合标准的编译器 可以 将
*dynamic_cast<T*>(ptr)
替换为*static_cast<T*>(ptr)
作为有效优化,我说得对吗?"usually" 是真的吗(意思是,在 x86 机器上,比方说,在 "usual" 复杂度的层次结构中 类 之间进行转换)一个
dynamic_cast
不能被优化掉,并且 实际上 会产生一个nullptr
即使你*
它紧随其后,导致nullptr
取消引用并在访问对象时崩溃?"always replace
*dynamic_cast<T*>(ptr)
with eitherdynamic_cast
+ test or assertion of some kind, or with*static_cast<T*>(ptr)
" 是一个合理的建议吗?
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
基的指针,您实际上可以 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
做不到的事情,当你从一个虚拟基础转换到派生的 class 时。)
在没有final
或全程序知识的世界中,您必须在运行时间进行检查(因为C
和D
可能不可见给你)。在 B
上使用 final
,你应该可以不这样做,但如果编译器还没有开始优化这种情况,我并不感到惊讶。