【问题标题】:c++ reinterpret_cast, virtual, and templates ok?c++ reinterpret_cast、virtual 和 templates 好吗?
【发布时间】:2012-02-19 14:06:20
【问题描述】:

在 C++ 中,假设以下类层次结构:

class BaseClass { };
class ChildClass : public BaseClass { };

进一步假设这两个类的工厂类具有一个通用的模板化基类:

template<typename T>
class Factory {
public:
  virtual T* create() = 0;
};

class BaseClassFactory : public Factory<BaseClass> {
public:
  virtual BaseClass* create() {
    return new BaseClass(&m_field);
  }
private:
  SomeClass m_field;
};

class ChildClassFactory : public Factory<ChildClass> {
public:
  virtual ChildClass* create() {
    return new ChildClass(&m_field);
  }
private:
  SomeOtherClass m_field; // Different class than SomeClass
};

请注意,ChildClassFactoryBaseClassFactory 的大小/内部结构因字段不同而不同。

现在,如果有ChildClassFactory(或Factory&lt;ChildClass&gt;)的实例,我可以安全地将其转换为Factory&lt;BaseClass&gt;(通过reinterpret_cast)吗?

Factory<ChildClass>* childFactory = new ChildClassFactory();

// static_cast doesn't work - need to use reinterpret_cast
Factory<BaseClass>* baseFactory = reinterpret_cast<Factory<BaseClass>*>(childFactory);

// Does this work correctly? (i.e. is "cls" of type "ChildClass"?)
BaseClass* cls = baseFactory->create();

我知道您不能总是以这种方式转换模板类,但在这种特殊情况下,转换应该是安全的,不是吗?

我已经用 Visual C++ 2010 对其进行了测试,它确实可以工作。我现在的问题是这是否可以移植到其他编译器?

更新:由于存在一些混淆,让我再澄清一下在我的示例中什么(应该是)重要的:

  • ChildClassBaseClass 的子类
  • Factory&lt;BaseClass&gt; 的用户不知道将创建BaseClass 的哪个子类。他只知道BaseClass 已创建。
  • Factory&lt;T&gt; 没有自己的字段(除了 vtable)。
  • Factory::create()virtual

【问题讨论】:

  • 在您的代码的这个特定片段中,我没有看到任何错误。
  • 请注意,您的 BaseClassFactory 和您的 ChildClassFactory 类在任何方面都没有任何关系。此外,拥有Factory&lt;T&gt; 模板没有任何意义,因为每个模板实例都是不同的、不相关的类型,因此虚拟接口根本没有给您带来任何好处。
  • @SebastianKrysmanski C++ 代码发生工作并不意味着它不是完​​全错误的。而说它与Visual C++一起工作,就更没有意义了。
  • @SebastianKrysmanski:当我执行T* t = new T; delete t; t-&gt;foo(); 之类的操作并且它目前适用于我当前的编译器时,这是否意味着“它不可能完全错误”?
  • @SebastianKrysmanski:我的意思是您可以完全删除 Factory&lt;T&gt; 模板,您将获得完全相同的行为。

标签: c++ templates casting virtual reinterpret-cast


【解决方案1】:

不,不是。除了一些特殊情况外,您不能使用reinterpret_cast 的结果来回退内容:

ISO14882:2011(e) 5.2.10-7:

一个对象指针可以显式转换为一个对象指针 一种不同的类型。70 当类型为“指向 T1 的指针”的纯右值 v 是 转换为“指向 cv T2 的指针”类型,如果 T1 和 T2 都是标准布局,则结果为 static_cast(static_cast(v)) 类型(3.9)和T2的对齐要求不严格 T1 的那些,或者如果任何一种类型都是无效的。转换类型的prvalue “指向 T1 的指针”指向类型“指向 T2 的指针”(其中 T1 和 T2 是 对象类型和 T2 的对齐要求没有 比 T1 更严格)并返回其原始类型产生 原始指针值。任何其他此类指针的结果 转换未指定。

为了使可能的失败场景更加清晰,请考虑多重继承,其中使用static_castdynamic_cast 有时会调整指针值,但reinterpret_cast 不会。考虑在此示例中从 A* 转换为 B*

struct A { int x; };
struct B { int y; };
struct C : A, B { };

要了解您的代码如何以不同的方式失败,请考虑大多数编译器如何实现虚拟函数调用机制:使用虚拟指针。您的ChildClassFactory 实例将有一个虚拟指针,指向ChildClassFactory 的虚拟表。现在当你reinterpret_cast 这个野兽时,它只是碰巧“工作”了,因为编译器需要一些虚拟指针,指向一个具有相同/相似布局的虚拟表。但它仍将包含指向ChildCLassFactory 虚函数的值,因此将调用这些函数。所有这一切都是在调用未定义行为之后很久。就好像你开着车跳进一个大峡谷,想着“嘿,一切都很好”,只是因为你还没有着地。

【讨论】:

  • @AndersK:我添加了一些解释为什么他的情况也会失败。
  • 但是Factory 没有基类,所以你的例子不适用。
  • 好吧,我同意@Sebastian Krysmanskis 代码确实构成未定义的行为,但是我还没有看到编译器无法运行此代码。并不是说我可以声称已经测试了所有这些。所以在我看来,“它是否可以移植到其他编译器?”这个问题的答案。是:可能,但它依赖于标准中未定义的行为。
  • @PeterT:它在很大程度上依赖于虚拟桌子布局之类的东西,以至于你在那里非常薄弱。 baseFactory-&gt;create(); 调用的函数是ChildClassFactory::create。这个调用可以通过添加虚函数、移动成员、稍微改变一些类型来快速销毁。这与使用内联汇编程序一样可移植。
  • @PlasmaHH:我认为我们正在取得进展。因此,实际的问题是,对于所有模板实例,模板的 vtable 是否总是以完全相同的布局构造(例如,指向 create 的指针始终是 vtable 中的第一个)。
【解决方案2】:

不,reinterpret_cast 仅用于低级代码,因为它不会执行正确的地址操作。请改用 static_cast 或 dynamic_cast,

为什么要两个不符合 GoF 工厂模式的工厂。

reinterpret_cast 不是这样做的方法,因为它很慢(运行时检查)并且不是一个好的 OO 设计(你想使用语言中的多态性)。

而是在工厂类中创建构造函数来生成您所追求的类型,然后让这些构造函数调用各个类型的构造函数。

工厂模式允许您对实现中的更改一无所知,这是一件好事,因为您可以最大限度地减少依赖关系,并允许在未来的代码中更轻松地维护。

【讨论】:

  • reinterpret_cast 不是“慢(运行时检查)”,因为它只是一个编译时技巧,告诉编译器以不同的方式解释位模式。不涉及运行时检查。此外,由于Factory&lt;ChildClass&gt;Factory&lt;BaseClass&gt; 是两种完全不相关的类型,将一个实例的位模式解释为另一个实例是不可移植的。这一次它可能会起作用,只是因为它们碰巧是相同的,但它们不是。
  • @SebastianKrysmanski:您对模板和类有误解。模板是模板,而不是类或类型。只有在实例化之后,它才会成为一个类型。但它不以任何方式连接到具有其他模板参数的类型。 Foo&lt;int&gt; 是与 Foo&lt;char&gt; 完全不同的类型。当您考虑可以专注于 `Foo´ 并使其真正变得非常不同时,这一点变得更加清晰。
  • @SebastianKrysmanski:模板参数的继承关系在这里完全不相关。模板只是模板(在该术语的标准英语含义中),因为它们提供了一种创建代码的机制。您可以使用您的代码并将 Factory&lt;CC&gt; 的派生替换为手写类 Factory_CC(对于 BC 也是如此),这两个类与模板具有相同的主体。从 C++ 的观点来看,这不会改变任何东西,但它可能会更清楚地表明 Factory_CC 和 Factory_BB 是完全不相关的。
  • @SebastianKrysmanski:所以你的意思是,在调用 UB 很久之后,你有一个幸运的情况,当你在当前版本的编译器中被解释为错误类型时,你得到一个偶然具有可用位模式的指针,与当前的编译设置?好吧,当这是您对 portable 的解释时,就这样吧,但对于世界上大多数人来说,“依赖于 UB 的不稳定行为”!=“便携”
  • @SebastianKrysmanski 简单的一点是,在 C++ 中,某段代码可以在一个编译器上完美运行,而在另一个编译器(或者只是你的下一个版本)上崩溃(甚至使宇宙内爆)编译器)。当您的代码调用 未定义的行为 时就是这种情况,在这种情况下,您的代码 错误 或至少 不可移植(这就是您的正在寻找)。
【解决方案3】:

我已经勾选了上面的原始答案(以表扬他),但我想我会总结一下我在这里学到的东西。

因此,基本问题是没有定义必须如何实现调度虚拟调用。

这意味着内部用于虚拟呼叫调度的数据结构(例如 vtables)可能或可能不在从同一模板创建的模板实例之间有点兼容。

【讨论】:

    猜你喜欢
    • 2012-04-10
    • 1970-01-01
    • 2011-06-17
    • 2011-06-10
    • 2017-12-16
    • 2012-12-15
    • 2019-08-27
    • 2021-09-25
    相关资源
    最近更新 更多