3

我有一个指向基类的指针数组,这样我就可以使这些指针指向基类的(不同的)子类,但仍与它们交互。(实际上只有几个方法我做了虚拟和重载)我想知道我是否可以避免使用指针,而只是制作一个基类的数组,但有一些方法可以将类设置为我的子类选择。我知道那里必须有一些东西指定类,因为它需要使用它来查找虚拟方法的函数指针。顺便说一句,子类都具有相同的 ivars 和布局。

注意:设计实际上是基于使用模板参数而不是变量,由于性能提高,所以抽象基类实际上只是子类的接口,除了它们的编译代码之外,它们都是相同的。

谢谢

编辑:所有子类(如果需要的话)基类具有相同的布局/大小

除此之外,策略模式会很好,但它添加了一个指向类的指针,而我试图避免的只是一种尊重。

简单来说,我想做的是

class base {
int ivars[100];
public:
virtual char method() = 0;
}

template <char c>
class subclass : base {
public:
  char method() {
    return c;
  }
}

base things[10];
some_special_cast_thingy(things[2], subclass);
printf("%c", things[2].method());

显然它要复杂得多,但就课程/任何事情而言,这几乎就是我所寻找的。顺便说一句,这可能是一种语言功能。

4

5 回答 5

2

使用指针和多态,这就是它们的用途。有一些开销,但除非您处于极其苛刻的环境中,否则它是微不足道的。这为您以后添加新子类提供了很大的灵活性。

于 2010-03-27T22:42:55.033 回答
2

您遇到的问题与存储分配有关。分配数组时,它们需要包含所有元素的存储空间。让我举一个(高度简化的)例子。假设您有这样的课程设置:

class Base
{
public:
  int A;
  int B;
}

class ChildOne : Base
{
public:
  int C;
}

class ChildTwo : Base
{
public:
  double C;
}

当您分配 aBase[10]时,数组中的每个元素将需要(在典型的 32 位系统*上)8 字节的存储空间:足以容纳两个 4 字节的整数。但是,一个ChildOne类需要其父类的 8 个字节的存储空间,以及其成员的额外 4 个字节C。一个ChildTwo类需要其父类的 8 个字节,再加上其double C. 如果您尝试将这两个子类中的任何一个推送到分配给 8-byte 的数组中Base,您最终会溢出存储空间。

指针数组起作用的原因是它们的大小是恒定的(在 32 位系统上每个 4 个字节),无论它们指向什么。指向 aBase的指针与指向 a 的指针相同ChildTwo,尽管后者的大小是后者的两倍。

dynamic_cast运算符允许您执行类型安全的向下转换以将 更改Base*为 a ChildTwo*,因此它将在这种特殊情况下解决您的问题。

或者,您可以通过创建类似这样的类布局来将处理逻辑与数据存储(策略模式)分离:

class Data
{
public:
  int A;
  int B;

  Data(HandlerBase* myHandler);
  int DoSomething() { return myHandler->DoSomething(this) }
protected:
  HandlerBase* myHandler;
}

class HandlerBase
{
public:
  virtual int DoSomething(Data* obj) = 0;
}

class ChildHandler : HandlerBase
{
public:
  virtual int DoSomething(Data* obj) { return obj->A; }
}

这种模式适用于算法逻辑DoSomething可能需要大量对象共有的重要设置或初始化(并且可以在ChildHandler构造中处理)但不通用(因此不适合静态成员)的情况. 然后数据对象保持一致的存储并指向将用于执行其操作的处理程序进程,当它们需要调用某些东西时将它们自己作为参数传递。Data这种类型的对象具有一致的、可预测的大小,并且可以分组到数组中以保持引用局部性,但仍然具有通常继承机制的所有灵活性。

请注意,您仍在构建相当于指针数组的内容 - 它们只是位于实际数组结构下方的另一层。

* 对于挑剔者:是的,我意识到我为存储分配提供的数字忽略了类头、vtable 信息、填充和大量其他潜在的编译器注意事项。这并不意味着详尽无遗。

编辑第二部分:以下所有材料都不正确。我在没有测试的情况下将它从头顶上发布,并将 reinterpret_cast 两个不相关的指针的能力与转换两个不相关的类的能力混淆了。Mea culpa,感谢 Charles Bailey 指出我的失态。

一般效果仍然是可能的——你可以强行从数组中抓取一个对象并将其用作另一个类——但它需要获取对象地址并强制将指针强制转换为新的对象类型,这违背了理论的目的避免指针取消引用。无论哪种方式,我最初的观点 - 这是一个可怕的“优化”,首先要尝试进行 - 仍然成立。

编辑:好的,我认为通过您的最新编辑,我已经弄清楚了您要做什么。我将在这里给你一个解决方案,但是,为了所有神圣的爱,请向我发誓,你永远不会在生产代码中使用它。这是一种工程好奇心,而不是一个好的做法。

您似乎试图避免进行指针取消引用(可能作为性能微优化?),但仍然希望在对象上调用子方法的灵活性。如果您确定您的基类和派生类的大小相同——您要知道这一点的唯一方法是检查编译器生成的物理类布局,因为它可以根据需要进行各种调整,并且规范没有给你任何保证——那么你可以使用 reinterpret_cast 强制将父级视为数组中的子级。

class Base
{
public:
  int A;
  int B;

  void DoSomething();
}

class Derived : Base
{
  void DoSomething();
}

void DangerousGames()
{
  // create an array of ten default-constructed Base on the stack
  Base items[10];
  // force the compiler to treat the bits of items[5] as a Derived,
  // and make a ref
  Derived& childItem = reinterpret_cast<Derived>(items[5]);
  // invoke Derived::DoSomething() using the data bits of items[5], 
  // since it has an identical layout
  childItem.DoSomething();
}

这将为您节省指针取消引用,并且没有性能损失,因为 reinterpret_cast 不是运行时强制转换,它本质上是一个编译器覆盖,它说:“无论你认为你知道什么,我都知道我在做什么,闭嘴并做吧。” “轻微的缺点”是它使您的代码非常脆弱,因为对Baseor布局的任何更改Derived,无论是您还是编译器发起的,都会导致整个事情在火焰中崩溃,可能会发生什么极其微妙且几乎不可能调试未定义的行为。同样,永远不要在生产代码中使用它。即使在性能最关键的实时系统中,指针取消引用的成本也总是与在您的代码库中间构建相当于触发核弹的东西相比,这是值得的。

于 2010-03-27T23:18:04.620 回答
1

这听起来像是状态模式或策略模式。

从类中分离出数据并让数组包含数据和指向基类的指针。设置指针,使其根据需要指向不同的子类,但基类方法采用指针或对数组元素的引用来访问数据。子类实现基类方法,但除了传递给方法的数据之外没有其他数据。

class Base;

class Data{
  string name;
  int age;
  Base* base;
};

class Base{
  virtual void method1(Data&)=0;
  // other methods
};

class Subclass1: public Base{
  void method1(Data& data){ data.age=0; }
};

vector<Data> dataVector;
Subclass1 subclass1;
Data* someData = ...
someData->base=&subclass1;

someData->base->method1(*someData);
于 2010-03-27T22:55:03.333 回答
0

您可以使用指针数组。如果你想访问部分派生类接口,你可以使用 dynamic_cast。http://en.wikipedia.org/wiki/Dynamic_cast

于 2010-03-27T22:46:41.093 回答
0

数组必须是精确类型,因为编译器必须能够计算每个元素的精确位置。因此,您不能混合使用子类,甚至不能将子类数组视为基类数组。

于 2010-03-27T22:48:59.057 回答