`dynamic_cast` 的描述不够严谨
Mq-b opened this issue · comments
写的是:
dynamic_cast
用于多态类型的转换
执行行运行时类型检查
只适用于指针或引用
对不明确的指针的转换将失败(返回 nullptr),但不引发异常
可以在整个类层次结构中移动指针,包括向上转换、向下转换
dynamic_cast< 新类型 >( 表达式 )
如果 表达式 是到多态类型 Base
的指针或引用,且 新类型 是到 Derived
类型的指针或引用,那么会进行运行时检查。
除此之外其他时候基本上是没有这种额外开销的。
并且它也可以用作其他的转换。
struct X { };
struct Y :X {};
int main() {
Y* y = new Y;
auto p = dynamic_cast<X*>(y); // 无虚函数 子类转父类,毫无问题。
auto p2 = static_cast<X*>(y); // 同
}
无虚函数,自然没有所谓的运行时检查。
当然了,没开销的时候说明不该使用 dynamic_cast
。
感觉应该改成
dynamic_cast
dynamic_cast
常用于多态类型的转换,如果是多态类型的话:
执行行运行时类型检查
只适用于指针或引用
对不明确的指针的转换将失败(返回 nullptr),但不引发异常
如果转型失败且 新类型 是引用类型,那么它会抛出与类型 std::bad_cast 的处理块匹配的异常
可以在整个类层次结构中移动指针,包括向上转换、向下转换
我觉得采用“运行时检查”的概念就很不好,混淆了语义和实现,应该抛弃之。
dynamic_cast<T *>(v)
的良构问题,忽略 cv 限定和转换为 void *
的话:
- 如果
T
不是完整类类型或者v
的类型不是指向完整类类型的指针,则非良构; - 如果转换类不变,则良构;
- 如果从派生类到基类:
- 如果基类无歧义且可访问,则良构;
- 否则,非良构;
- 其他情况:
- 如果
v
的类型是指向多态类型的指针,则良构; - 否则,非良构。
- 如果
良构情况的语义问题(C++ 现行标准的文本写得很复杂,因为引入了“运行时检查”的概念,下面这个版本等效):
- 如果
v
是nullptr
则结果是nullptr
,否则继续; - 找出
v
的最派生对象u
; - 找出
u
的所有T
基类子对象t_1, ..., t_n
; - 若存在惟一的
1<=i<=n
使t_i
和v
有基类子对象关系(t_i
是v
的基类子对象或v
是t_i
的基类子对象或v
就是t_i
),则结果是指向t_i
的指针(这里不需要n=1
); - 否则,若
n=1
,则结果是指向t_1
的指针; - 否则,结果是
nullptr
。
C++ 标准的规定:
- 除了类不变、派生类到基类,都要求多态类型;
- 除了类不变、派生类到基类、任意类到
void *
、从nullptr
转换,都算是“运行时检查”。
这里的重点在于:
- 从多态类型出发的转换不一定有“运行时检查”,比如从多态类型到它的基类;
- “运行时检查”不一定是从基类到派生类,也可以是表面上没有继承关系的类。
因此
dynamic_cast< 新类型 >( 表达式 )
如果 表达式 是到多态类型Base
的指针或引用,且 新类型 是到Derived
类型的指针或引用,那么会进行运行时检查。
这个说法不够全面。而
如果是多态类型的话:
执行行运行时类型检查
这个说法和现行 C++ 标准不一致。
最后,从实现效率考虑,假设编译器对 v
的情况一无所知,并采用通常的实现:
- 如果类不变,则没有任何运行时开销;
- 如果是从派生类到非
virtual
基类,且基类的 offset 是 0,则没有任何运行时开销; - 如果是从派生类到非
virtual
基类,且基类的 offset 不是 0,则运行时需要判断nullptr
并条件加减数; - 如果是从派生类到
virtual
基类,则运行时需要判断nullptr
并进行某些 indirection,这个开销可能比 5 低,也可能和 5 一起处理; - 如果是其他转换,则会有较高的运行时开销。
仓库原文里
对不明确的指针的转换将失败
这句话本身就很不明确:
- 类不变、派生类到基类,和最派生对象的其他基类子对象没有关系;
- 基类到派生类,因为语义里步骤 4 的规则,如果
v
指向的对象确实是某个派生类对象的一部分,则以被转换的指针所指向的对象为基类子对象的情况优先,这时最派生对象可以含有其他T
基类子对象; - 其他情况,最派生对象必须有惟一的
T
基类子对象。
应该特别注意,从基类到派生类,有两种模式(取决于 v
具体指向最派生对象的哪个基类子对象)。
仓库原文里
可以在整个类层次结构中移动指针,包括向上转换、向下转换
不够全面——可以向上、向下、旁支转换。
还应该注意,派生类转基类,如果基类是 virtual
无歧义可访问,那么 static_cast
不可以,但 dynamic_cast
可以,此时也不需要派生类是多态类型(具有 virtual
基类并不会导致类型成为多态类型,只有 virtual
函数才会导致)。
除了类不变、派生类到基类、任意类到 void *、从 nullptr 转换,都算是“运行时检查”。
学到了🤣。
不过能详细聊一下嘛,以及
“运行时检查”不一定是从基类到派生类,也可以是表面上没有继承关系的类
能举个例子嘛?
其实另外一部分就提到了
不够全面——可以向上、向下、旁支转换。
如下:
struct B1 { virtual f1();};
struct B2 { virtual f2();};
struct D: B1, B2 {};
void fun(B1 *p) {
auto p = dynamic_cast<B2*>(p);
}
此处从B1*
向B2*
的sidecast就是
“运行时检查”不一定是从基类到派生类,也可以是表面上没有继承关系的类
这个转换要藉由struct D
这样的旁支选择无歧义的时候才能进行
@Mq-b 后面问题的例子:
struct B1 { virtual ~B1(); };
struct B2 { };
struct D : B1, B2 { };
D d;
B1 *b1 = &d;
// B1 和 B2 表面上没有继承关系
// b1 是 B1 * 而 B1 是多态类型
// b1 指向对象的最派生对象是 d
// d 里面的 b1 是公开基类 B1 的子对象
// d 有无歧义基类 B2 且 B2 是 D 的公开基类
// 转换得到这个 B2 基类子对象的指针
B2 *b2 = dynamic_cast<B2 *>(b1);
@dynilath 的例子没有体现 B2
不需要是多态类型,另外
这个转换要藉由
struct D
这样的旁支选择无歧义的时候才能进行
准确来说,是不考虑访问性时选择无歧义且基类关系公开,注意有惟一公开基类、不惟一基类的时候,转换失败。
第一部分的问题,单纯是 C++ 标准,见 expr.dynamic.cast,下面假设 dynamic_cast<C *>(v)
且 v
是 V *
且 C
和 V
都是完整类类型且 v
不等于 nullptr
,无关的内容都略:
- 略;
- 略;
- 如果
C
就是V
,则结果是v
; - 如果
C = B
是V = D
的基类,那么结果是v
的惟一B
基类子对象;如果D
的基类B
歧义或者不可访问,则程序非良构; - 否则,
V
必须是多态类型; - 略(从
nullptr
转换); - 略(转换到
void *
); - 进行“运行时检查”,设
v
指向对象的最派生对象是u
:
a. 如果v
指向u
的某个C
基类子对象c
的某个公开基类子对象(用v <= c <= u
表示),且不存在不是c
的c'
使v <= c' <= u
且c'
是C
,则结果是指向c
的指针;
b. 如果v
指向u
的某个公开基类子对象,且u
具有惟一的C
基类子对象c
,且C
是公开基类,则结果是指向c
的指针; - 略。
这里比较变态的点是关于 public
和可访问性的细节,我的“等效”表述里面忘记考虑。But still, 这个标准相当难读,而且有不少“陷阱”。
上面的叙述里“运行时检查”排除了 2、3、6、7。
我懒,而且这只是不幸的选词,而不是技术问题。
而且这只是不幸的选词,而不是技术问题。
……正因此我建议通过 editorial issue 处理,如果是技术问题反而不该这么做。