简体   繁体   中英

Pattern for storing multiple types of struct in a C++ std::<vector> container

I have a data structure which represents a train, which can be made up of many types of car, for example the train engines, a grain car, a passenger car, and so on:

struct TrainCar {
   // ...
   Color color;
   std::string registration_number;
   unsigned long destination_id;
}

struct PowerCar : TrainCar {
   // ...
   const RealPowerCar &engine;
}

struct CargoCar : TrainCar {
   // ...
   const RealCargoCar &cargo;
   bool full;
}

std::vector<TrainCar*> cars;
cars.push_back(new TrainCar(...));
cars.push_back(new TrainCar(...));
cars.push_back(new CargoCar(...));
cars.push_back(new CargoCar(...));
cars.push_back(new CargoCar(...));

An algorithm will iterate through the cars in the train, and decide how to route/shunt each car (whether to keep it in the train, move it to another point in the train, remove it from the train). This code looks like:

std::vector<TrainCar*>::iterator it = cars.begin();
for (; it != cars.end(); ++it) {
    PowerCar *pc = dynamic_cast<PowerCar*>(*it);
    CargoCar *cc = dynamic_cast<CargoCar*>(*it);

    if (pc) {
        // Apply some PowerCar routing specific logic here
        if (start_of_train) {
            // Add to some other data structure
        }
        else if (end_of_train && previous_car_is_also_a_powercar) {
            // Add to some other data structure, remove from another one, check if something else...
        }
        else {
            // ...
        }
    }
    else if (cc) {
        // Apply some CargoCar routing specific logic here
        // Many business logic cases here
    }
}

I am unsure whether this pattern (with the dynamic_casts, and chain of if statements) is the best way to process the list of simple structs of varying types. The use of dynamic_cast seems incorrect.

One option would be to move the routing logic to the structs (so like (*it)->route(is_start_of_car, &some_other_data_structure...)), however I'd like to keep the routing logic together if possible.

Is there a better way of iterating through different types of simple struct (with no methods)?, or do I keep the dynamic_cast approach?

The standard solution to this is called double-dispatch . Basically, you first wrap your algorithms in separate functions that are overloaded for each type of car:

void routeCar(PowerCar *);
void routeCar(CargoCar *);

Then, you add a route method to car that is pure virtual in the base-class, and implemented in each of the subclasses:

struct TrainCar {
   // ...
   Color color;
   std::string registration_number;
   unsigned long destination_id;

   virtual void route() = 0;
}

struct PowerCar : TrainCar {
   // ...
   const RealPowerCar &engine;
   virtual void route() {
       routeCar(this);
   }
}

struct CargoCar : TrainCar {
   // ...
   const RealCargoCar &cargo;
   bool full;
   virtual void route() {
       routeCar(this);
   }
}

Your loop then looks like this:

std::vector<TrainCar*>::iterator it = cars.begin();
for (; it != cars.end(); ++it) {
    (*it)->route();
}

If you want to choose between different routing-algorithms at run-time, you can wrap the routeCar -functions in an abstract base class and provide different implementations for that. You would then pass the appropriate instance of that class to TrainCar::route .

If the number of classes is manageable, you can give a try to boost::variant .

Using "sum types" in C++ is sometimes a mess, so it is either this or double dispatching.

The classical OO solution would be to make all of the relevant functions virtual in the base class TrainCar , and put the concrete logic in each class. You say, however, that you'd like to keep the routing logic together if possible. There are cases where this is justified, and the classical solution in such cases is a variant union ( boost::variant , for example). It's up to you to decide which is better in your case.

Compromises are possible as well. For example, one can easily imagine a case where the routing logic is somewhat independent of the car type (and you don't want to duplicate it in each car type), but it does depend on a certain number of characteristics of the car type. In this case, the virtual function in TrainCar could simply return an object with the necessary dependent information, to be used by the routing algorithm. This solution has the advantage of reducing the coupling between the routing and TrainCar to the minimum necessary.

Depending on the nature of this information, and how it is used, the returned object could be polymorphic, with it's inheritance hierarchy reflecting that of TrainCar ; in this case, it must be allocated dynamically, and managed: std::auto_ptr was designed with exactly this idiom in mind.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM