【问题标题】:Updating data members of different derived classes of the same base class within a vector更新向量内同一基类的不同派生类的数据成员
【发布时间】:2015-11-01 21:38:09
【问题描述】:

我正在用 C++ 编写一个具有不同单元类型的 3D 网格模型,所有单元都存储在 Grid 类中的向量中。我已经定义了一个基础 GridCell 类,并且我还有两个派生类 GridCell1 和 GridCell2。

现在在设置模型时,我读入了一个文本文件,它告诉我如何在 Grid 类中填充我的 gridCell 向量 (std::vector<gridCell*> gridCellVector);意思是它告诉我将哪些类型的派生单元推回我的 gridCellVector。

然后我读入另一个输入文件,该文件包含我的 Grid 中每个 GridCell 的初始状态变量信息,按第一个输入文件的顺序排列。

每个派生类(GridCell1 和 GridCell2)都有一些其他的没有的状态变量(私有数据成员)。当我在第二个输入文件中读取时,我如何(或可能)访问和更新/初始化/设置派生类的数据成员?

我尝试了几种不同的方法,但似乎只能返回 GridCell 基类中定义的 get/set 函数。在处理每个派生的 GridCell 时,我不知道如何访问派生类中的函数,因为我单步执行了向量。

编辑:我很惊讶人们没有提到向下转换,只是说不使用 dynamic_cast。我总是知道我正在更新的 GridCell 的类型,因为我会在读取第一个输入文件时跟踪已加载到向量中的内容。由于我总是确定 GridCell 的类型,dynamic_cast 不安全吗?

双重编辑:。因为我将 GridCell 对象传递给需要引用特定于传递对象的适当 GridCell 实例的数据成员和函数的其他函数,所以我正在实现设计(许多部分)我的模型目前没有通过集合。所以,现在,我完全放弃了必须使用 GridCell 类型的想法,而只会创建一个满足我所有需求的巨大 GridCell 类。这样我就可以填充,然​​后访问我以后需要的任何数据成员和函数。

【问题讨论】:

  • 你能同时扫描两个文件吗?
  • 我可以,但是我的一些设置函数会变得笨重且太大。可能很难将它们分解......有没有办法在两步过程中做到这一点?
  • 太大且笨重?我认为它实际上会给你最简单、最优雅的代码。如果您真的想要(例如使用dynamic_cast),您可以分两步完成,但我不建议这样做。

标签: c++ inheritance vector


【解决方案1】:

如果你确定要使用两步过程,我建议你给GridCell一个纯虚拟的init方法:

virtual void init(istream &) = 0;

然后在每个派生类中实现它。其目的是从文件中读取数据并初始化初始状态变量。

【讨论】:

  • 我会更进一步,将其设为virtual void 方法,因此派生类获取数据并可以做它需要做的事情,然后他可以做gridCell::init,而基类类也会获取数据以备不时之需。
  • @NicFoster:是的,我在文中说“虚拟”,将其设为虚拟才是重点,但我在代码中忽略了它。这是漫长的一天。正在编辑...
  • 感觉从ifstream 初始化不应该是GridCell 的责任。它不是很可测试的一件事。此外,每次更改文件格式时,都必须更改 GridCell。至少让它使用istream 而不是ifstream,这样它的可测试性就更高了。
  • 这真是一个非常糟糕的计划。
  • @Puppy:如果你知道一个更好的,考虑到 OP 对两阶段解决方案的偏好,请赐教。
【解决方案2】:

单程

正如其他人所说,最好同时读取两个文件并在创建派生类的同时进行派生类特定的初始化:

std::unique_ptr<GridCell> createGridCell1(std::istream& init) {
    auto cell = std::make_unique<GridCell1>();
    int value; 
    init >> value;
    cell->setGridCell1State(value);   
    return cell;
}

std::unique_ptr<GridCell> createGridCell2(std::istream& init) {
    // similarly to CreateGridCell1()...
}

std::vector<GridCell::Ptr> createCells(std::istream& types, std::istream& init) {
    std::vector<GridCell::Ptr> cells;
    std::string type;
    while (types >> type) {
        if (type == "GridCell1")
            cells.push_back(createGridCell1(init));
        else
            cells.push_back(createGridCell2(init));
    }
    return cells;
}

int main() {
    auto types = std::istringstream("GridCell1 GridCell2 GridCell1 GridCell1");
    auto init = std::istringstream("1 2.4 2 3");

    auto cells = createCells(types, init);

    for (auto& cell : cells) 
        cell->put();
}

Live demo.

与访客的两次通行证

如果您必须在第二遍中进行初始化,您可以使用Visitor pattern。你有某种GridCellVisitor 知道如何访问所有不同类型的网格单元:

class GridCellVisitor {
protected:
    ~GridCellVisitor() = default;
public:
    virtual void visit(GridCell1& cell) = 0;
    virtual void visit(GridCell2& cell) = 0;
};

并且您的网格单元知道如何接受GridCellVisitor:

class GridCell1 : public GridCell {
    int state = 0;
public:
    void setGridCell1State(int value) { state = value; }
    void accept(GridCellVisitor& visitor) override { visitor.visit(*this); }
};

class GridCell2 : public GridCell {
    double state = 0.0;
public:
    void setGridCell2State(double value) { state = value; }
    void accept(GridCellVisitor& visitor) override { visitor.visit(*this); }
};

通过这种方式,您可以将使用输入流初始化网格单元的职责与网格单元本身分开,并避免对网格单元进行脆弱的向下转换:

class GridCellStreamInitializer : public GridCellVisitor {
    std::istream* in;
public:
    GridCellStreamInitializer(std::istream& in) : in(&in){}
    void visit(GridCell1& cell) override { 
        int value; 
        *in >> value;
        cell.setGridCell1State(value);
    }

    void visit(GridCell2& cell) override {
        double value;
        *in >> value;
        cell.setGridCell2State(value);
    }
};

int main() {
    auto in = std::istringstream("GridCell1 GridCell2 GridCell1 GridCell1");
    auto cells = createCells(in);

    auto init = std::istringstream("1 2.4 2 3");
    auto streamInitializer = GridCellStreamInitializer(init);
    for (auto& cell : cells)
        cell->accept(streamInitializer);
}

Live demo.

缺点是GridCellVisitor 必须了解所有不同类型的网格单元,因此如果添加新类型的网格单元,则必须更新访问者。但据我了解,您读取初始化文件的代码无论如何都必须了解所有不同类型的网格单元。

【讨论】:

  • 这似乎没问题,除了答案中的所有细节和代码都显示了第二个糟糕的解决方案,而不是第一个解决方案。
  • @Puppy 我为第一个解决方案添加了一些示例代码,但如果要求它必须分两次完成,我实际上认为访问者模式是合理的适合。
【解决方案3】:

您的vector&lt;gridCell*&gt; 只知道其元素的基类,因此只能调用gridCell 函数。

我了解您的方法是首先用指向正确派生类型的单元格的指针填充向量,而不是基本类型。然后对于每个单元格,您读取依赖于类的数据。

最简单的方法,如果你不想改变方法

最简洁的方法是在基本单元中定义一个虚拟加载函数:

class gridCell {
   ...
   virtual bool load (ifstream &ifs) {
       // load the common data of all gridCells and derivates
       return ifs.good();  
   } 
};

虚函数将被派生单元覆盖:

class gridCell1 : public gridCell {
   ...
   bool load (ifstream &ifs) override {
       if (gridCell::load(ifs)) {  // first load the common part
          // load the derivate specific data 
       }
       return ifs.good();  
   } 
};

最后,你可以编写你的容器加载函数了:

class Grid {
    ... 
    bool load (ifstream &ifs) {
        for (auto x:gridCellVector) 
            if (!x->load(ifs))
               break;  // error ? premature end of file ? ... 
    } 
 };

最干净的方法?

您的问题看起来很像序列化问题。你加载网格,可能你也写网格吗?如果您控制文件格式,并一次性完成单元格的创建和加载,则无需重新发明轮子,可以选择序列化库,例如 boost::serialization .

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-02-05
    • 1970-01-01
    • 2022-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-02
    相关资源
    最近更新 更多