强制转换**→基本**错误吗?有什么选择?

时间:2019-02-11 14:11:43

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

上下文

我的目标是要有一个包含和操纵几个基类对象的基本容器类,然后是一个包含和操纵多个派生类对象的派生容器类。根据{{​​3}}的建议,我尝试通过使每个指针包含一个指针数组(一个startTrace(name:)和一个Base**)并从Derived**到{{ 1}}初始化基本容器类。

但是,我遇到了一个问题–尽管编译得很好,但是在处理包含的对象时,我会遇到段错误或将调用错误的方法


问题

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

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 最终被调用了。我也遇到了带有更复杂类的段错误,因此我认为它是地址相关的hijinks(也许是vtable相关的-如果没有Base2::doThing2方法,该错误似乎不会发生)。您可以this answerrun it here

see the assembly it compiles to here –更为复杂,但将其与上下文相关联,并解释了为什么我需要类似的东西。

为什么Base1::doThing1virtual进行时,Derived**Base**投射为什么不能正常工作,更重要的是,是将派生对象数组作为基础对象数组处理的正确方法(否则,是制作包含多个派生对象的容器类的另一种方法)?

恐怕我无法在向上转换(Derived*)之前建立索引,因为在完整的代码中,数组是类成员–而且我不确定这是一个好主意(甚至可能)手动在每个位置进行强制转换,在容器类中使用包含的对象。不过,请纠正我是否 是处理此问题的方法。

(在这种情况下,我认为这没有什么区别,但是我处于Base*不可用的环境中。)


编辑:解决方案

许多答案表明,分别转换每个指针是拥有可以包含派生对象的数组的唯一方法-确实确实如此。不过,对于我的特定用例,我设法使用模板解决了该问题!通过提供容器类应该包含的内容的类型参数,而不是必须包含派生对象的数组,可以在编译时将数组的类型设置为派生类型(例如((Base2*) derivedPtrs[0])->doThing2()

You can see my actual structure here

5 个答案:

答案 0 :(得分:2)

有很多事情使这段代码变得很糟糕,并助长了这个问题:

  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的指针的数组,那么您必须手动转换每个指针。由于两个数组中的指针值通常是不同的,因此无法“重用”同一数组。

(除非满足empty base optimization的条件,但您却不是这种情况。)

答案 1 :(得分:2)

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

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

然后&derived指向对象的起始位置,

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

编译器知道Base2结构在Derived类型内具有的偏移量,并调整地址以指向其开头。

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

在您遇到的情况下可能无法正常工作的肮脏黑客正在使用一种将指针返回自身的方法,例如:

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

以便您可以进行derivedPtrs[0]->base2()->doThing2()

答案 2 :(得分:1)

它不起作用的原因相同:

git clone https://github.com/DivanteLtd/vue-storefront.git vue-storefront && cd vue-storefront
npm install
npm install vue-carousel vue-no-ssr
npm run build # check if no errors
npm run installer

您的C样式转换是一种不好的做法,因为它没有警告您错误。只是不要对您的代码这样做。您的C样式转换实际上是变相的struct Base {}; struct Derived : Base { int i; }; int main() { Derived d[6]; Derived* d2 = d; Base** b = &d2; // ERROR! } ,在这种情况下完全是错误的。

但是为什么不能将要派生的数组转换为要基于数组的数组?很简单:它们具有不同的布局。

您看到,当您迭代类型的数组时,数组中的每个元素在内存中都是连续的。 reinterpret_cast类的大小可以说是24个字节,Derived类的大小可以是8:

Base

如您所见,Derived d[4]; ------------------------------------------------------ | D1 | D2 | D3 | D4 | ------------------------------------------------------ Base b[4]; --------------------- | B1 | B2 | B3 | B4 | --------------------- Derived[4]是具有不同布局的不同类型。


那你能做什么?

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

它看起来像这样:

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*时进行强制转换。这样做比较困难,但是可以通过降低迭代速度来节省您的分配。


最重要的是,这可能无关紧要,但是我建议不要使用虚拟继承。这不是一个好习惯,以我的经验,这种痛苦比什么都重要。我建议使用适配器模式并包装非多态类型。

答案 3 :(得分:0)

当您从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();

答案 4 :(得分:0)

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

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

假设编译器使用“ vtable”,从Base2的内存地址中找到Derived的vtable(可能在内存中添加偏移量。assembly)为不平凡。

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