使用连续内存实现多态性



实际上我没有遇到"问题",因为我的代码确实可以工作。我只是好奇我的实现是否合理和无风险。

我一直在使用C++进行一个项目,在该项目中,我首先解析一个文件,然后相应地构建一个有向无循环图结构。根据节点的类型,每个节点可能有0~2个外邻居。对于不同类型的节点,需要一些用于打印和访问的功能,我决定使用多态性来实现。

我的第一个尝试是用存储指向其外部邻居的指针的节点来实现它。

class Base{
public:
Base(){}
virtual ~Base(){}
virtual foo()=0;
// ...
protected:
unsigned _index;
}
class Derived1: public Base{
public:
foo(){ /*Do something here...*/ }
private:
Base* _out1;
}
class Derived2: public Base{
public:
foo(){ /*Do something different here...*/ }
private:
Base* _out1;
Base* _out2;
}
int main(){
std::vector<Base*> _nodeList;
for(/*during parsing*/){
if(someCondition){
_nodeList.pushback(new Derived1);
}
// ...
}
}

由于节点的外邻居可能还没有定义何时构建节点,所以我必须添加一些技巧,首先记住外邻居的id,并在完成所有节点的构建后连接它们。

然而,由于节点的数量是在给定要解析的文件的情况下确定的,并且此后不会增长,因此我认为最好连续存储所有节点,每个节点存储其外邻居的索引,而不是指针。这让我可以跳过连接部分,也给其他部分带来了一些小好处。

我目前的版本如下:

// Something like this
class Base{
public:
Base(){}
virtual ~Base(){}
virtual foo()=0;
// ...
protected:
unsigned _index;
unsigned _out1;
unsigned _out2;
}
class Derived1: public Base{
public:
foo(){ /*Do something here...*/ }
}
class Derived2: public Base{
public:
foo(){ /*Do something a little bit different here...*/ }
}
int main(){
// EDITED!!
// Base* _nodeList = new DefaultNode[max_num];
Base* _nodeList = new Derived2[max_num];
for(/*during parsing*/){
if(someCondition){
// EDITED!!
// _nodeList[i] = Derived1;
new(_nodeList+i) Derived1();
}
// ...
}
}

我的问题

  1. newed数组中存储不同类的对象是否存在风险,因为它们都具有相同的大小,并且可以使用虚拟析构函数进行析构函数?

  2. 我一直听说应该避免使用new[]。我确实找到了一些方法,使用带有类型标签的unionvector来实现我想要的,但这对我来说似乎有些肮脏。有没有一种方法可以在std::vector中存储数据的同时实现多态性?

  3. 仅仅为了利用虚拟函数的便利性而使用多态性的做法是否被认为是一种坏习惯?我这么说的意思是,如果每个对象占用的内存对于每个派生类来说已经是相同的,那么它们可能会合并到一个单独的类中,存储自己的类型,并且每个成员函数可以根据自己的类型进行操作。我选择不这样做,因为在每个成员函数中都有巨大的switch结构对我来说也很肮脏。

  4. 在这种情况下,选择连续内存好吗?这样的选择可能有害,有什么原因吗?

编辑:

事实证明,我犯了很多错误,比如一次问太多问题。我想我将首先关注多态性和位置新的部分。下面是一个可测试的程序,我所说的"将不同派生类的对象存储在一个新的数组中",它在我的笔记本电脑上的行为如下所示

#include <iostream>
class Base{
public:
Base(){}
virtual ~Base(){}
void virtual printType() =0;
};
class Derived1: public Base{
public:
Derived1(){}
void printType(){ std::cout << "Derived 1." << std::endl; }
};
class Derived2: public Base{
public:
Derived2(){}
void printType(){ std::cout << "Derived 2." << std::endl; }
};
int main(){
Base* p = new Derived1[5];
new(p+2) Derived2();
for(unsigned i = 0; i < 5; ++i){
(p+i)->printType();
}
}

结果:

Derived 1.
Derived 1.
Derived 2.
Derived 1.
Derived 1.

再次感谢所有的反馈和建议。

  1. 在一个新数组中存储不同类的对象是否有任何风险,因为它们的大小都相同并且可以被销毁使用虚拟析构函数

这不是你的第二个命题:

Base* _nodeList = new DefaultNode[max_num];

_nodeList是一个DefaultNote的数组,没有其他!尝试在其中存储类似_nodeList[i] = ...的内容将永远不会改变存储对象的性质(请注意,_nodeList[i] = Derived1;不是C++)。如果您想要多态性,您需要通过指针或引用来保留对象。那么第一个解决方案是正确的:std::vector<Base*> _nodeList;

  1. 我一直听说应该避免使用new[]。我确实找到了一些方法来实现我想要的使用并集向量带有type标签,但在我看来有点脏。有办法吗在std::向量中存储数据时实现多态性

应该避免使用new[]是没有意义的。如前所述,如果您需要多态性,那么std::vector<Base*> _nodeList;是完美的,因为这意味着您可以在_nodeList中存储类为Base或其任何子类型的任何对象的地址。

  • 使用多态性的做法仅仅是为了利用虚拟功能的便利性被认为是一种坏习惯?我这么说如果每个对象占用的内存已经相同派生类,则它们可以合并为一个单独的类存储自己的类型,每个成员函数可以根据到自己的类型。我选择不这么做,因为它在我看来也很脏在每个成员函数中都有巨大的开关结构
  • 子类型多态性虚拟函数的使用。为什么是坏习惯?如果你不使用虚拟函数,那就意味着你自己在构建多态性,这可能是一件非常糟糕的事情。

    现在,如果你的派生类和你的示例中提出的一样,我可以建议你不要使用子类,而只使用ctor重载。。。

    1. 在这种情况下选择连续内存好吗?这样的选择可能有害,有什么原因吗

    我不确定为什么这是你关心的问题。连续内存无害。。。这个问题至少还不清楚。

    问题是,通常不能在向量或数组中分配不同的多态类,只能分配指向它们的指针。所以你不能使它连续。

    在您的情况下,使用多态性很可能是个坏主意。由于大量虚拟调用和分支预测失败的问题,它将导致较差的内存碎片和较慢的性能。不过,如果节点不多,或者你在代码中不太频繁地使用它,那么它不会影响程序的整体性能。

    为了避免这种情况,只需将节点的数据存储在向量中(并使其成为一个普通结构),并使用单独的类来实现这些foo()函数。

    示例:

    std::vector<NodeData> nodes;
    class Method1
    {
    public:
    static void Process(NodeData& node);
    ...
    }
    class Method2
    {
    public:
    static void Process(NodeData& node);
    ...
    }
    

    然后,您可以进行单个切换来选择应用哪种方法,也可以将节点的数据存储在几个向量中,这样每个向量都可以识别要使用的方法。

    最新更新