Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dynamic_cast 的描述不够严谨 #113

Open
Mq-b opened this issue Oct 16, 2023 · 10 comments
Open

dynamic_cast 的描述不够严谨 #113

Mq-b opened this issue Oct 16, 2023 · 10 comments

Comments

@Mq-b
Copy link

Mq-b commented Oct 16, 2023

写的是:

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 的处理块匹配的异常
可以在整个类层次结构中移动指针,包括向上转换、向下转换

@GeeLaw
Copy link

GeeLaw commented Nov 1, 2023

我觉得采用“运行时检查”的概念就很不好,混淆了语义和实现,应该抛弃之。

dynamic_cast<T *>(v) 的良构问题,忽略 cv 限定和转换为 void * 的话:

  • 如果 T 不是完整类类型或者 v 的类型不是指向完整类类型的指针,则非良构;
  • 如果转换类不变,则良构;
  • 如果从派生类到基类:
    • 如果基类无歧义且可访问,则良构;
    • 否则,非良构;
  • 其他情况:
    • 如果 v 的类型是指向多态类型的指针,则良构;
    • 否则,非良构。

良构情况的语义问题(C++ 现行标准的文本写得很复杂,因为引入了“运行时检查”的概念,下面这个版本等效):

  1. 如果 vnullptr 则结果是 nullptr,否则继续;
  2. 找出 v 的最派生对象 u
  3. 找出 u 的所有 T 基类子对象 t_1, ..., t_n
  4. 若存在惟一的 1<=i<=n 使 t_iv 有基类子对象关系(t_iv 的基类子对象或 vt_i 的基类子对象或 v 就是 t_i),则结果是指向 t_i 的指针(这里不需要 n=1);
  5. 否则,若 n=1,则结果是指向 t_1 的指针;
  6. 否则,结果是 nullptr

C++ 标准的规定:

  • 除了类不变、派生类到基类,都要求多态类型;
  • 除了类不变、派生类到基类、任意类到 void *、从 nullptr 转换,都算是“运行时检查”。

这里的重点在于:

  • 从多态类型出发的转换不一定有“运行时检查”,比如从多态类型到它的基类;
  • “运行时检查”不一定是从基类到派生类,也可以是表面上没有继承关系的类。

因此

dynamic_cast< 新类型 >( 表达式 )
如果 表达式 是到多态类型 Base 的指针或引用,且 新类型 是到 Derived 类型的指针或引用,那么会进行运行时检查。

这个说法不够全面。而

如果是多态类型的话:
执行行运行时类型检查

这个说法和现行 C++ 标准不一致。

最后,从实现效率考虑,假设编译器对 v 的情况一无所知,并采用通常的实现:

  1. 如果类不变,则没有任何运行时开销;
  2. 如果是从派生类到非 virtual 基类,且基类的 offset 是 0,则没有任何运行时开销;
  3. 如果是从派生类到非 virtual 基类,且基类的 offset 不是 0,则运行时需要判断 nullptr 并条件加减数;
  4. 如果是从派生类到 virtual 基类,则运行时需要判断 nullptr 并进行某些 indirection,这个开销可能比 5 低,也可能和 5 一起处理;
  5. 如果是其他转换,则会有较高的运行时开销。

仓库原文里

对不明确的指针的转换将失败

这句话本身就很不明确:

  • 类不变、派生类到基类,和最派生对象的其他基类子对象没有关系;
  • 基类到派生类,因为语义里步骤 4 的规则,如果 v 指向的对象确实是某个派生类对象的一部分,则以被转换的指针所指向的对象为基类子对象的情况优先,这时最派生对象可以含有其他 T 基类子对象;
  • 其他情况,最派生对象必须有惟一的 T 基类子对象。

应该特别注意,从基类到派生类,有两种模式(取决于 v 具体指向最派生对象的哪个基类子对象)。

仓库原文里

可以在整个类层次结构中移动指针,包括向上转换、向下转换

不够全面——可以向上、向下、旁支转换。


还应该注意,派生类转基类,如果基类是 virtual 无歧义可访问,那么 static_cast 不可以,但 dynamic_cast 可以,此时也不需要派生类是多态类型(具有 virtual 基类并不会导致类型成为多态类型,只有 virtual 函数才会导致)。

@w272628569
Copy link

w272628569 commented Nov 1, 2023 via email

@w540665710
Copy link

w540665710 commented Nov 1, 2023 via email

@DaweiWangBIT
Copy link

DaweiWangBIT commented Nov 1, 2023 via email

@Mq-b
Copy link
Author

Mq-b commented Nov 1, 2023

除了类不变、派生类到基类、任意类到 void *、从 nullptr 转换,都算是“运行时检查”。

学到了🤣。

不过能详细聊一下嘛,以及

“运行时检查”不一定是从基类到派生类,也可以是表面上没有继承关系的类

能举个例子嘛?

@GeeLaw

@dynilath
Copy link

dynilath commented Nov 1, 2023

其实另外一部分就提到了

不够全面——可以向上、向下、旁支转换。

如下:

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这样的旁支选择无歧义的时候才能进行

@GeeLaw
Copy link

GeeLaw commented Nov 1, 2023

@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)vV *CV 都是完整类类型且 v 不等于 nullptr,无关的内容都略:

  1. 略;
  2. 略;
  3. 如果 C 就是 V,则结果是 v
  4. 如果 C = BV = D 的基类,那么结果是 v 的惟一 B 基类子对象;如果 D 的基类 B 歧义或者不可访问,则程序非良构;
  5. 否则,V 必须是多态类型;
  6. 略(从 nullptr 转换);
  7. 略(转换到 void *);
  8. 进行“运行时检查”,设 v 指向对象的最派生对象是 u
    a. 如果 v 指向 u 的某个 C 基类子对象 c 的某个公开基类子对象(用 v <= c <= u 表示),且不存在不是 cc' 使 v <= c' <= uc'C,则结果是指向 c 的指针;
    b. 如果 v 指向 u 的某个公开基类子对象,且 u 具有惟一的 C 基类子对象 c,且 C 是公开基类,则结果是指向 c 的指针;
  9. 略。

这里比较变态的点是关于 public 和可访问性的细节,我的“等效”表述里面忘记考虑。But still, 这个标准相当难读,而且有不少“陷阱”。

上面的叙述里“运行时检查”排除了 2、3、6、7。

@frederick-vs-ja
Copy link

考虑到“运行时检查”出现在标准文本中,我们是不是该考虑下提个编辑 issue@GeeLaw

@GeeLaw
Copy link

GeeLaw commented Nov 14, 2023

我懒,而且这只是不幸的选词,而不是技术问题。

@frederick-vs-ja
Copy link

而且这只是不幸的选词,而不是技术问题。

……正因此我建议通过 editorial issue 处理,如果是技术问题反而不该这么做。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants