Is polymorphism the best way to achieve this? (regarding function calls in derived classes)

Viewed 67

I have a program that has four classes:

  • Vehicle (Base)
  • Automobile (Derived from Vehicle)
  • Car (Derived from Automobile)
  • Truck (Derived from Automobile)

At run-time, the user generates a Automobile object which is either a "Car" or "Truck" using a factory function:

Automobile *Automobile::make_automobile(std::string choice) {
    if (choice == "car")
        return new Car;
    else if (choice == "truck")
        return new Truck;
    else
        return nullptr;
}

Now, the "Car" class has three unique setters (and three matching getters):

  • Set_NumDoors()
  • Set_SeatMaterial()
  • Set_Shape()

The "Truck" class has one unique setter (and a matching getter):

  • Set_CargoWeight()

At first, these functions were only implemented in their own classes, but I could not call them at run-time since the object created was an "Automobile" object. I have solved this issue using virtual functions with local overrides. So now, the "Automobile" class all four functions (in virtual form), which by default do nothing. However, when called on an object, the local override performs the correct functionality. Below is an example, and it all works.

Definition in Automobile class

std::string defaultStatement = "Function call is invalid on current object";
    virtual int set_seatMaterial(std::string choice){
        std::cout << defaultStatement << std::endl;
        return -1;
    }

Override in Car class

    int set_seatMaterial(std::string choice) override { // override virtual base
        if (choice == "leather" || choice == "cloth"){
            seatMaterial = choice;
            return 0;
        }
        else
            return -1;
    }

I then use function pointers in main() to point to the required function as appropriate:

if (user_choice == "seat"){
            std::function<int(Automobile*, std::string)> choiceFunction = &Automobile::set_seatMaterial;
            choiceFunction(userVehicle, seatMaterial);
}

The only question I have is - is this the best way of achieving this functionality? It works but now I have declarations of every single function in the "Automobile" class, which already has its own getters/setters. It seems like duplication in a sense, although I understand the concept of polymorphism and its usefulness.

Or is there a better way to call a derived class function from a base class object?

2 Answers

I have solved this issue using virtual functions with local overrides. So now, the "Automobile" class all four functions (in virtual form), which by default do nothing. However, when called on an object, the local override performs the correct functionality. Below is an example, and it all works.

That makes it necessary for the implentor of Automobile to know all the methods that all the classes that inherits from it provides which makes it hard to maintain.

Another option is to dynamic_cast in the situations where you need to know what type of Automobile you're dealing with.

C++11 example:

Automobile* unknown = ...;

if(auto car = dynamic_cast<Car*>(unknown)) {
    // call car-specific methods
    car->Set_NumDoors(4);

} else if(auto truck = dynamic_cast<Truck*>(unknown)) {
    // call truck-specific methods
    truck->Set_CargoWeight(1000);
}

Demo

Your question indeed concerns a principal design problem. Generally spoken, polymorphism is most adequate if the objects can usually be treated identically. The exact type of the object then needs to be known only in a few specific situations. In other words, there is a significant common subset of behavior that is common to all cars, in your example, and most of the times we don't even want to know which sort of car we have here. If you find yourself programming Ted's if-else chains over and over again it may be an indication that the vehicles are different enough to warrant separate processing. If you find that it is so, you would for example not have a vector of pointers to cars but instead separate vectors of trucks and automobiles and semis and what not, which are processed separately.

But let's assume that polymorphism is a valid approach here because there is only a limited number of situations where each type of car needs special treatment: When you buy a car, when you repair one, when you dispose of one. For example, an automobile buyer may want to choose the seat material while the company buying a truck doesn't have that choice. There are, obviously, many more things that may be different int the respective car "setup".

The solution here is to specify an abstract car base class method for each of the respective situations. For example, each car would have a "setup" routine which interacts with the user and obtains the required subtype-specific information. So instead of the if-else chain you rely on polymorphism again to do the right thing for you. This works if you can fit all the different behaviors into a uniform abstract pattern which you describe by abstract methods in the base class. The downside is that the objects may need information about their environs (an iostream, a graphics context, database access, whatever) which has nothing really to do with their actual function. We would principally like to keep these things separate from the cars for the sake of weak component coupling. So here, too, this approach works best if the objects are similar enough that they need a similar set of information which can be passed to the common method (instead of being known to the respective car object).

As a conclusion, here is a general rule of thumb: While Ted's answer is not wrong one should always be suspicious of dynamic casts and if-else chains; they are often a sign of lacking abstraction.

Related