当你使用多重继承时,什么情况下使用static_cast是安全的?

15

我发现自己遇到了这样一种情况:我知道某个类型的类型属于三个(或更多)继承级别之一。我调用一个工厂,它返回 B*,但是如果我的代码已经知道 T 的类型,则 T 可能是类型的最高级别(或第二级别)。

总之,在模板中我使用了 static_cast,这是错误的做法。我的问题是什么时候可以安全地进行静态强制转换?有没有这样的时候?在这种情况下,我这样做是因为当我无意中将 T 设为奇怪的东西时,我宁愿得到编译错误,而动态转换会忽略它并返回 null。但是,当我知道正确类型时,指针未被调整,导致我有一个错误的指针。我不确定为什么在这种情况下允许使用静态转换。

在什么情况下可以安全地使用静态转换进行向下转换?有没有这样的情况?现在似乎总是错误的使用 static_cast 进行向下转换(目的是向下转换)。

好的,我找到了再现它的方法。

#include <iostream>
struct B { virtual void f1(){} };
struct D1 : B {int a;};
struct D2 : B {int a, b; };
struct DD : D1, D2 {};

int main(){
void* cptr = new DD(); //i pass it through a C interface :(
B*  a = (B*)cptr;
D2* b = static_cast<D2*>(a); //incorrect ptr
D2* c = dynamic_cast<D2*>(a); //correct ptr
std::cout << a << " " <<b << " " <<c;
}

请更加具体一些。你能提供一个代码示例吗?我大致知道你在说什么,但不确定。通常情况下,如果你转换到正确的类型,进行静态转换向上转型是“安全”的。 - Vaughn Cato
@Vaughn:我不能展示代码(这是我在工作中遇到的问题),但其中一个问题涉及多重继承和将类用作接口。 - user34537
3
需要阅读的内容:对于每个使用“upcast”一词的人,必须了解向上转换(upcast)是从派生类型向其基类之一进行转换 - Ben Voigt
你能提供一个代码示例来演示问题吗?最好是一些新的代码,专门用于演示。 - Vaughn Cato
哎呀,这看起来像是虚拟继承的情况。这几乎总是会变得非常混乱,而且几乎总是可以避免的 - 你确定你真的需要这个吗?例如,不要让DD从D1和D2继承,为什么不使用组合?(struct DD : B {D1 d1; D2 d2;};) - Ayjay
@acidzombie24:非常有帮助。我的答案如下。 - Vaughn Cato
6个回答

15

叉式转换:

struct Base1 { virtual void f1(); };
struct Base2 { virtual void f2(); };
struct Derived : Base1, Base2 {};

Base1* b1 = new Derived();
Base2* b2 = dynamic_cast<Base2*>(b1);

需要使用dynamic_cast,不能使用static_caststatic_cast应该会导致编译时错误)。如果任一基类不是多态的,则dynamic_cast也会失败(虚函数的存在是必须的)。

请参阅MSDN上的解释


啊哈+1。我想提一下,在我的情况下,不幸的是它没有导致编译错误。 - user34537
只要基础类型未被转换为void指针,也未使用reinterpret_cast引起指针调整混淆(尤其是在存在歧义时),那么static_cast是安全的。假设存在编译错误并且被强制转换的类型确实是正确的类型。 - user34537
1
@acidzombie24:不,即使从void*转换,使用static_cast进行多重继承转换也是不安全的。 - Ben Voigt
为什么使用static_cast进行多重继承转换时不安全?我认为如果我们确定b1是一个Derived,那么执行Base2* b2 = static_cast<Derived*>(b1)是安全的。感到困惑。 - legends2k
1
@legends2k:你没有进行交叉转换。交叉转换并不知道涉及哪种“Derived”类型,只要对象具有继承了“Base1”和“Base2”的某种类型即可。 - Ben Voigt
显示剩余2条评论

5
如果DerivedBase作为公共(或其他可访问的)基类,并且d的类型为Derived*,那么static_cast<Base*>(d)就是一个向上转型
这在技术上总是安全的。
通常情况下不必使用,除非存在方法隐藏(遮蔽)的情况。
祝好!

在进行向上转换时,使用强制类型转换是不必要的,必要时会自动进行转换。 - Ben Voigt
@Ben 很有趣。我从未听说过“交叉转换”。 - user34537
2
@acidzombie24:抱歉,你所描述的不是交叉转换(cross-cast)。交叉转换是指,当你有一个实际上同时继承 Base1Base2 的对象时,你从 Base1Base2 进行 dynamic_cast,但是 Base1Base2 本身并没有关联。但是 Alf 关于向上转换的说法是完全正确的。我认为你想问的不是向上转换,而是向下转换。 - Ben Voigt
2
@acidzombie24:进一步阅读您的问题表明,您可能正在谈论交叉转换(实际上是向下转换后跟随向上转换),只是没有解释清楚。即使不是您的实际代码,您真的需要提供至少一个简化的示例来说明您的意思。 - Ben Voigt
1
当我回答这个问题时,标题是“何时使用静态转换进行向上转换是安全的?”,并且没有代码。我不感兴趣追逐SO历史更改... - Cheers and hth. - Alf
显示剩余4条评论

3
问题出在这行代码上:
B*  a = (B*)cptr;

如果您将某个对象转换为void指针,必须在进行任何其他转换之前先将其转换回与其相同的类型。 如果您有多个不同类型的对象必须通过相同的void指针传递的情况,那么您需要先将其缩小到一个公共类型,然后再转换为void指针。

int main(){
  B *bptr = new DD; // convert to common base first (won't compile in this case)
  void* cptr = bptr; // now pass it around as a void pointer
  B*  a = (B*)cptr; // now back to the type it was converted from
  D2* b = static_cast<D2*>(a); // this should be ok now
  D2* c = dynamic_cast<D2*>(a);  // as well as this
  std::cout << a << " " <<b << " " <<c;
}

编辑: 如果你只知道在转换时cptr指向B的派生类型对象,那么这并不足够。当你试图将DD指针转换为B指针时,编译器会让你知道这一点。

你需要做的是像这样:

int main(){
  void* cptr = new DD; // convert to void *
  DD* a = (DD*)cptr; // now back to the type it was converted from
  D2* b = static_cast<D2*>(a); // this should be ok now, but the cast is unnecessary
  D2* c = dynamic_cast<D2*>(a);  // as well as this
  std::cout << a << " " <<b << " " <<c;
}

但我不确定这是否符合你的实际使用要求。

你运行了代码吗?新的DD实际上会导致编译错误,因为它是模糊的。我确定b仍然是一个不正确的指针,因为它不知道必须进行调整(它不识别实际类型,因此在D2之前已知D1)。 - user34537
@acidzombie24:不,我没有,你是对的。你想做的事情行不通,编译器也在提醒你。 - Vaughn Cato
我已经添加了一些内容,希望能提供一些有用的东西。 - Vaughn Cato
我不知道实际类型(因此无法将其强制转换为DD),但问题是何时使用static_cast是安全的。在我的情况下绝对不是。我想到的是,只有在没有多重继承并且我知道实际派生类型时才可以使用。这几乎从来没有发生过。 - user34537
3
static_cast 并不是不安全的,问题在于将其转换为 void 指针,然后再转回不同类型的指针是不安全的。如果没有涉及到 void 指针,这就不会成为问题。 - Vaughn Cato
这只是因为你不能在不使用void指针(或使用reinterpret_cast)的情况下转换该类型。我想只要您的源指针可以在不使用上述两个指针的情况下获得,那么它就是安全的。嗯......我还没有说过,但+1。 - user34537

2

如果您确定对象实际上是该类的实例,则可以安全地向上转换。

class Base {};
class Derived1 : public Base {};
class Derived2 : public Base {};

int main()
{
    Base* b = new Derived1;

    Derived1* d1 = static_cast<Derived1*>(b); // OK
    Derived2* d2 = static_cast<Derived2*>(b); // Run-time error - d isn't an instance of Derived2
}

我对Alf说的话。即使你的情况是真实的(在我的场景中也是如此),它仍然会导致问题(特别是在使用多重继承和类作为“接口”时)。 - user34537
2
向上转型是Base* d = new Derived1;,而且不可能有Derived1不是Base的实例。 - Ben Voigt
啊,我总是混淆向上转换和向下转换。我认为向上转换与其他类型转换不太相似。 - Ayjay
@acidzombie24:什么问题? - Ayjay

2
只是为了完整性(知道我有点晚了,只是为了像我这样的迟到读者……):
如果使用正确,static_cast可以应用!
首先,看一个简单的例子:
struct D1 { }; // note: no common base class B!
struct D2 { };
struct DD : D1, D2 { };

你可以通过中间向下转型到 DD*,从 D1* 到达 D2*
D1* d1 = new DD();
D2* d2 = static_cast<DD*>(d1);

向上转换到D2*是隐式的。即使对于非虚拟继承也是可能的。但请注意,在进行向下转换时,您需要确保d1确实被创建为DD,否则会导致未定义的行为!现在来看更复杂的情况:钻石模式!这就是问题所在。
void* cptr = new DD();
B* a = (B*)cptr;

现在这个类型转换已经很危险了!实际上,在这里执行的是reinterpret_cast。
B* a = reinterpret_cast<B*>(cptr);

您所需要的是一个简单的向上转换。通常情况下,不需要进行转换:
B* a = new DD(); //ambigous!

仅有:DD 有两个继承自 B 的实例。如果 D1D2 都是虚拟继承自 Bstruct D1/2 : virtual B { }; - 不要与 B/D1/D2 是虚拟类混淆!),那么它就可以工作了。
B* b1 = static_cast<D1*>(new DD());
B* b2 = static_cast<D2*>(new DD());

将文本翻译成中文:

将分别转换为基类D1D2,现在清楚了应该指向哪个继承的B实例。

您现在可以通过再次向下转换为DD来获取另一个相应的实例;由于菱形模式,您需要再次进行中间转换:

D2* bb1 = static_cast<DD*>(static_cast<D1*>(b1));
D1* bb2 = static_cast<DD*>(static_cast<D2*>(b2));

关键点在于:当进行向下转型时,必须使用与向上转型时相同的钻石边界!!!
结论:使用静态转换是可能的,并且如果所涉及的类不是虚拟类(注意:要与虚拟继承区分开来!),这是唯一的选择。但是,正确执行它太容易失败,有时甚至是不可能的(例如,在 std::vector 中存储基类型指针到任意派生类型),因此通常提供了 dynamic_cast 解决方案,如 Ben 所述,这是更安全的选择(如果可用虚拟数据类型,则在前面提到的 vector 示例中,它是唯一的解决方案!)。

1
一个交叉转换根本不需要动态转换(dynamic_cast)...
   struct Base1 { virtual void f1(); };
   struct Base2 { virtual void f2(); };
   struct Derived : Base1, Base2 {};

   Base1* b1 = new Derived();

   // To cast it to a base2 *, cast it first to a derived *
   Derived *d = static_cast<Derived *>(b1);
   Base2 *b2 = static_cast<Base2 *>(d);

5
可以使用 static_cast 进行下行转换后的上行转换(第二个 static_cast 应该省略),但直接的交叉转换不能这样做。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接