`dynamic_cast` 的描述不够严谨
写的是:
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 转换,都算是“运行时检查”。
学到了🤣。
不过能详细聊一下嘛,以及
“运行时检查”不一定是从基类到派生类,也可以是表面上没有继承关系的类
能举个例子嘛?
@GeeLaw
其实另外一部分就提到了
不够全面——可以向上、向下、旁支转换。
如下:
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。
考虑到“运行时检查”出现在标准文本中,我们是不是该考虑下提个编辑 issue?@GeeLaw
我懒,而且这只是不幸的选词,而不是技术问题。
而且这只是不幸的选词,而不是技术问题。
……正因此我建议通过 editorial issue 处理,如果是技术问题反而不该这么做。