【问题标题】:Is casting Derived** → Base** wrong? What's the alternative?铸造 Derived** → Base** 错了吗?有什么选择?
【发布时间】:2019-02-11 14:11:43
【问题描述】:

上下文

我的目标是拥有一个包含和操作多个基类对象的基容器类,然后是一个包含和操作多个派生类对象的派生容器类。在this answer 的建议下,我尝试通过让每个指针都包含一个指针数组(Base**Derived**)来做到这一点,并在初始化基本容器类时从Derived** 转换为Base**

然而,我遇到了一个问题——尽管编译得很好,但在操作包含的对象时,我会遇到段错误,或者会调用 错误的方法


问题

我将问题归结为以下最小情况:

#include <iostream>

class Base1 {
public:
    virtual void doThing1() {std::cout << "Called Base1::doThing1" << std::endl;}
};

class Base2 {
public:
    virtual void doThing2() {std::cout << "Called Base2::doThing2" << std::endl;}
};

// Whether this inherits "virtual public" or just "public" makes no difference.
class Derived : virtual public Base1, virtual public Base2 {};

int main() {
    Derived derived;
    Derived* derivedPtrs[] = {&derived};
    ((Base2**) derivedPtrs)[0]->doThing2();
}

您可能希望这会打印“Called Base2::doThing2”,但是……

$ g++ -Wall -Werror main.cpp -o test && ./test
Called Base1::doThing1

确实——代码调用Base2::doThing2,但Base1::doThing1最终被调用。我在更复杂的类中也遇到了这个段错误,所以我认为它是与地址相关的 hijink(可能与 vtable 相关——如果没有 virtual 方法,错误似乎不会发生)。你可以run it heresee the assembly it compiles to here

You can see my actual structure here – 它更复杂,但它与上下文相关联并解释了为什么我需要类似的东西。

为什么Derived**Base** 转换在Derived*Base* 可以正常工作,更重要的是,处理数组的正确方法是什么将派生对象作为基对象数组(或者,如果做不到这一点,另一种方法可以创建可以包含多个派生对象的容器类)?

恐怕我无法在向上转换 (((Base2*) derivedPtrs[0])-&gt;doThing2()) 之前建立索引,因为在完整代码中,数组是类成员 - 我不确定转换是否是个好主意(甚至可能)手动在容器类中使用包含的每个位置的对象。不过,如果 处理这个问题的方法,请纠正我。

(我认为在这种情况下不会有什么不同,但我处于std::vector 不可用的环境中。)


编辑:解决方案

许多答案表明,单独强制转换每个指针是拥有一个可以包含派生对象的数组的唯一方法——而且似乎确实如此。不过,对于我的特定用例,我设法使用 templates 解决了这个问题!通过为容器类应该包含的内容提供类型参数,而不是必须包含派生对象的数组,可以在编译时将数组的类型设置为派生类型(例如BaseContainer&lt;Derived&gt; container(length, arrayOfDerivedPtrs);)。

Here's a version of the broken "actual structure" code above, fixed with templates.

【问题讨论】:

  • 虚拟基地真的很重要。
  • 你为什么要选角?举个例子,因为doThing1()doThing2() 都是虚拟的,所以不需要强制转换。这就是虚成员函数的全部意义所在。
  • @obskyr 在这种情况下,您没有正确地进行继承,并且您的示例也没有足够详细地说明您的问题。按照公式,您的问题的答案是“不要施放”。如果您尝试创建自己的 std::variant,则标记联合可能是更有用的解决方案。
  • @obskyr:不,因为这听起来根本不像是一个有用的继承模型。 “Base1 的容器”和“Base1”之间没有 is-a 关系,因此对这种关系建模没有意义。
  • 我不同意; “一组 X”不是“一个 X”;您碰巧对它们都应用了一些通用操作这一事实并没有改变这一事实。您也可以设置一个重载集 foo(InputChannel&amp;)foo(InputChannelSet&amp;),这将允许您完成相同的事情,而不会在 InputChannelInputChannelSet.

标签: c++ casting containers multiple-inheritance vtable


【解决方案1】:

有很多因素使这段代码非常糟糕并导致了这个问题:

  1. 为什么我们首先要处理两星类型?如果std::vector 不存在,为什么不自己写呢?

  2. 不要使用 C 风格的强制转换。您可以将指向完全不相关类型的指针相互转换,并且不允许编译器阻止您(巧合的是,这正是这里发生的事情)。请改用static_cast/dynamic_cast

  3. 让我们假设我们有std::vector,以便于表示。您正在尝试将 std::vector&lt;Derived*&gt; 转换为 std::vector&lt;Base*&gt;。这些是不相关的类型(Derived**Base** 也是如此),将一个类型转换为另一个类型在任何情况下都是不合法的。

  4. 从/到派生的指针转换不一定是微不足道的。如果您有struct X : A, B {},那么指向B 基址的指针将不同于指向A 基址 的指针(并且在使用vtable 时,也可能与指针不同到X)。它们必须是,因为(子)对象不能驻留在相同的内存地址。当您转换指针时,编译器将调整指针值。如果您(尝试)转换一个指针数组,这当然不会/不可能发生在每个单独的指针上。

如果您有一个指向Derived 的指针数组,并且想要获得一个指向Base 的指针数组,那么您必须手动转换每个指针。由于两个数组中的指针值通常不同,因此无法“重用”同一个数组。

(除非满足empty base optimization 的条件,而你的情况并非如此)。

【讨论】:

  • 那么您认为解决我的容器继承问题 (see real-world structure here) 的最佳方法是,在初始化派生容器时,手动将每个指针转换为 for 循环并初始化基础有那个容器?
  • 这是解决这个问题的一种方法。它是否是解决它的最佳方法取决于更多现实世界的约束(例如所有权?),以及这是否是要解决的正确问题(或者您是否应该避免具有如此多虚拟继承的设计)是另一回事。我不能给你一个普遍的祝福,对不起:)
  • OP 的代码是否也会受到对象切片的影响?
  • @KristopherIves 当错误的赋值运算符(静态类型之一)用于分配具有不同动态类型的对象实例时,会发生对象切片,例如Base&amp; refToBase = derivedInstance; refToBase = otherDerivedInstance;(只会复制otherDerivedInstanceBase 部分),或delete 的类似情况。由于在呈现的代码中没有这样的分配或删除,因此从技术上讲不存在对象切片。缺少虚拟析构函数意味着它可能会成为一个问题,但我相信该示例没有包含它以便最小化。
【解决方案2】:

就像其他人说的那样,问题在于您没有让编译器完成调整索引的工作,假设内存中的派生布局类似于(标准不保证,只是一种可能的实现):

| vtable_Base1 | Base1 | vtable_Base2 | Base2 | vtable_Derived | Derived |

然后&amp;derived 指向对象的开头,当你通常这样做时

Base2* base = static_cast<Derived*>(&derived)

编译器知道Base2结构在Derived类型中的偏移量,并调整地址指向它的开始。

如果您直接转换一个指针数组,编译器会强制转换该类型,假设您的数组已经存储了指向 Base2 的指针,但没有对其进行调整。

在您的情况下可能有效或无效的肮脏黑客方法是有一个返回指向自身的指针的方法,例如:

class Base2 {
public:
  Base2* base2() { return this; }
}

这样你就可以derivedPtrs[0]-&gt;base2()-&gt;doThing2()

【讨论】:

  • 很好的答案!我只会更明确地说明您显示的布局不能保证所有编译器/架构。
  • 啊!了解 vtable 如何在内部工作实际上有很大帮助。明白了——这就是当你上投指针时会发生的事情。现在要弄清楚如何在我的实际情况下解决这个问题 - 我会看看你建议的黑客是否有帮助。
【解决方案3】:

它不起作用的原因与它不起作用的原因相同:

struct Base {};

struct Derived : Base { int i; };

int main() {
    Derived d[6];
    Derived* d2 = d;
    Base** b = &d2; // ERROR!
}

您的 c 样式转换是一种不好的做法,因为它没有警告您错误。只是不要对您的代码这样做。您的 c 风格演员实际上是伪装的 reinterpret_cast,这在这种情况下是完全错误的。

但是为什么你不能将一个数组转换成一个数组到基数呢?很简单:它们有不同的布局。

你看,当你迭代一个类型的数组时,数组中的每个元素在内存中都是连续的。 Derived 类的大小可能是 24 字节,Base 的大小是 8:

Derived d[4];
------------------------------------------------------
|     D1     |     D2     |     D3     |      D4     |
------------------------------------------------------

Base b[4];
---------------------
| B1 | B2 | B3 | B4 |
---------------------

如您所见,Derived[4]Base[4] 是不同的类型,具有不同的布局。


那你能做什么呢?

其实有很多解决方案。最简单的方法是创建一个指向基的新指针数组,并将每个派生的指针转换为基指针。无论如何,您必须调整每个对象的指针。

看起来像这样:

std::vector<Base*> bases;
bases.reserve(std::size(derived_arr))

std::transform(
    std::begin(derived_arr), std::end(derived_arr),
    std::back_inserter(bases), 
    [](Dervied* d) {
        // You must use dynamic cast because the 
        // pointer offset in only known at runtime
        // when using virtual inheritance
        return dynamic_cast<Base*>(d); 
    }
);

内存中的另一个解决方案是创建您自己的迭代器类型,在调用operator*operator-&gt; 时执行强制转换。这有点难做,但可以通过使迭代变慢一些来节省分配。


最重要的是,这可能无关紧要,但我建议不要使用虚拟继承。这不是一个好的做法,根据我的经验,这比任何事情都更痛苦。我建议使用适配器模式并包装非多态类型。

【讨论】:

  • C 风格演员是 reinterpret_cast&lt;const_cast&lt;Type&gt;&gt;,而不仅仅是 reinterpret_cast&lt;Type&gt;
  • @MarkIngram 一个 C 风格的演员 可以是 reinterpret_cast&lt;const_cast&lt;Type&gt;&gt;。在这种情况下,没有const_cast
  • @MarkIngram 它并不总是reinterpret_cast。有时是static_cast。这取决于被转换的类型。在这种情况下,没有const_cast
  • 是的,这里没有,因为可变性没有改变 - 我只是指出 C-Style 演员有能力做到这两点,以使 OP 受益。跨度>
【解决方案4】:

当您从Derived* 转换为Base* 时,编译器将调整该值。当您将 Derived** 转换为 Base** 时,您就打败了它。

这是始终使用static_cast 的好理由。更改代码的示例错误:

test.cpp:19:5: error: static_cast from 'Derived **' to 'Base2 **' is not
      allowed
    static_cast<Base2**>(derivedPtrs)[0]->doThing2();

【讨论】:

  • “当您从Derived* 转换为Base* 时,编译器将调整该值”——如何以及为什么?为什么不将Derived** 改为Base**?还有……你知道我可以做些什么来解决这个问题吗?
  • @obskyr “为什么?” -> 子对象在派生对象中一个接一个地出现,因此它们必须具有不同的(如果子对象非空,则增加)指针值。您的 Base 对象本身没有子对象,但它们有一个 vtable,因此它们是非空的。 “如何?” -> 编译器知道BaseDerived 中的位置。
  • 感谢您的解释!
【解决方案5】:

(Base2 **) 实际上是reinterpret_cast(您可以通过尝试所有四个转换来确认这一点)并且该表达式会导致 UB。您可以将指向派生的指针隐式转换为指向基址的指针这一事实并不意味着它们是相同的,例如intfloat。在这里,您通过它不是的类型来引用对象,在这种情况下,会导致 UB。

以这种方式调用虚函数会导致调用该对象的最终覆盖器。这如何实现取决于编译器。

假设编译器使用“vtable”,从Derived 的内存地址找到Base2 的vtable(可能是向内存添加偏移量。assembly)并非易事。

基本上,您必须对每个指针执行动态转换,将它们存储在某处或在需要时动态转换它。

【讨论】:

  • “该类型的最终覆盖”没有意义,您可能意味着 some 虚函数的最终覆盖声明了该类(哪个取决于确切的布局)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-08-17
  • 1970-01-01
  • 1970-01-01
  • 2011-01-09
  • 1970-01-01
  • 2012-06-01
  • 1970-01-01
相关资源
最近更新 更多