Reputation: 637
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?
Upvotes: 4
Views: 2820
Reputation: 76788
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
.
Upvotes: 8
Reputation: 153919
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.
Upvotes: 0
Reputation: 56956
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.
Upvotes: 0