施法派生**→基地**有错吗?还有什么选择?

Is casting Derived** → Base** wrong? What's the alternative?

本文关键字:什么 选择 派生 基地 施法      更新时间:2023-10-16

上下文

我的目标是有一个包含并操作几个基类对象的基本容器类,然后是一个包含和操作几个派生类对象的派生容器类。根据这个答案的建议,我试图通过让每个指针都包含一个指针数组(Base**Derived**)来实现这一点,并在初始化基本容器类时从Derived**强制转换为Base**

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


问题

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

#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最终被调用。我在更复杂的类中也遇到过这种segfault,所以我认为它是与地址相关的hijink(可能与vtable相关——如果没有virtual方法,错误似乎不会发生)。您可以在此处运行它,并在此处查看它编译到的程序集。

你可以在这里看到我的实际结构——它更复杂,但它将它与上下文联系起来,并解释了为什么我需要这样的东西。

为什么Derived**Derived*Base**强制转换无法正常工作→Base*确实,更重要的是,将派生对象数组作为基对象数组来处理的正确方法是什么(否则,另一种方法可以创建一个可以包含多个派生对象的容器类)?

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

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


编辑:解决方案

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

这是上面损坏的"实际结构"代码的一个版本,用模板修复。

有很多事情让这个代码变得非常糟糕,并导致了这个问题:

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

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

  3. 为了便于记法,我们假设我们有std::vector。您正在尝试将std::vector<Derived*>强制转换为std::vector<Base*>。这些都是不相关的类型(Derived**Base**也是如此),将一个类型强制转换为另一个类型在任何方面都是不合法的。

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

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

(除非满足空基优化的条件,否则情况并非如此)。

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

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

然后&derived指向对象的起点,当您通常进行时

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

编译器知道CCD_ 31结构在CCD_

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

一个肮脏的黑客,可能在你的情况下工作或不工作,是有一个方法返回指针到自己,例如:

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

这样你就可以做CCD_ 34了。

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

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->时执行强制转换。这有点难做,但可以通过使迭代稍微慢一点来节省分配。


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

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();

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

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

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

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