Updating data members of different derived classes

2019-05-24 06:27发布

问题:

I am writing a 3D gridded model in C++ which has different cell types, all stored within a vector that is in a Grid class. I have defined a base GridCell class and I also have two derived classes GridCell1 and GridCell2.

Now in setting up the model, I read in a text file that tells me how to fill my gridCell vector (std::vector<gridCell*> gridCellVector) in the Grid class; meaning it tells me what types of derived cells to push_back into my gridCellVector.

Then I read in another input file that contains initial state variable information for each GridCell in my Grid, in the order laid out by the 1st input file.

Each derived class (GridCell1 and GridCell2) has some state variables (private data members) that the other doesn't. How can I (or is it possible to) access and update/initialize/set the derived class' data members as I read in the second input file?

I've tried a couple different things and seem only able to return my get/set functions defined in the GridCell base class. I can't figure out how to access the functions in the derived classes when working with each derived GridCell as I step through the vector.

Edit: I am surprised people haven't mentioned downcasting, other than saying not to use dynamic_cast. I always know the type of GridCell I am updating because I keep track of what has been loaded into the vector when reading in the first input file. Since i am always certain of the type of GridCell, isn't dynamic_cast safe?

Double Edit:. Because I pass the GridCell objects to other functions that need to reference the data members and functions specific to the appropriate GridCell instance of the passed object, I'm realizing the design (of many parts) of my model does not currently pass muster. So, for now, I'm giving up on the idea of having to ride the GridCelltypes at all and will just create one huge GridCell class that fits all my needs. This way I can fill, and then access, whatever data members and functions I need later on down the line.

回答1:

If you're sure you want to use a two-step process, I suggest you give GridCell a pure virtual init method:

virtual void init(istream &) = 0;

then implement it in each derived class. Its purpose is to read data from the file and initialize the initial state variables.



回答2:

Single pass

As others have said, it may be best to read both files at once and do the derived class specific initialization at the same time as creating the derived classes:

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.

Two pass with Visitor

If you must do the initialization in a second pass you could use the Visitor pattern. You have some sort of GridCellVisitor that knows how to visit all the different kinds of grid cells:

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

and your grid cells know how to accept a 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); }
};

This way you can separate the responsibility of initializing the grid cells with an input stream from the grid cells themselves and avoid having to do fragile downcasts on the grid cells:

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.

The downside is GridCellVisitor must be aware of all different kinds of grid cells so if you add a new type of grid cell you have to update the visitor. But as I understand it your code that reads the initialization file must be aware of all the different kinds of grid cells anyway.



回答3:

Your vector<gridCell*> knows only the base class of its elements and can hence only call gridCell functions.

I understand that your approach, is to first fill the vector with pointer to cells of the correct derived type, and never the base type. Then for each cell, you read class dependent data.

The easiest way, if you don't want to change approach

The cleanest way would be to define a virtual load function in the base cell:

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

The virtual function would be overriden by teh derived cells:

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

Finally, you can write your container loading function:

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

The cleanest way ?

Your problem looks very much like a serialisation problem. You load grids, may be you write grids as well ? If you control the file format, and perform the creation and loading of the cells in a single pass, then you don't need to reinvent the wheel and could opt for a serialisation library, like boost::serialization.