Search code examples
c++ooptemplatespolymorphism

What are the pros and cons of template and polymorphism based approaches in C++?


I wrote a program where the type of a variable is not known at compile time. It is asked to the user at run time. In the first program version, a part of the code was repeated. The modifications concerned the type of variable used. In second and third program versions, a template-based approach and a polymorphism-based approach (with abstract class and virtual function) were used.
All programs run fine. But I wonder what are the pros and cons of this two approaches, when one is most appropriate than the other and if another approaches exist ?

To clarify my question, this an example below :
First version (with code repetition)

#include <iostream>
using namespace std;

class Shape {
    public : float lgth;
    public : void getLgth() { cin >> lgth;}
};

class Square : public Shape {
    public : float calcArea() { return lgth * lgth; }
};

class Circle : public Shape {
    public : float calcArea() { return 3.14 * lgth * lgth; }
};

int main() {
    char shape_type;

    cout <<"Shape Type (c/s) ? ";
    cin >> shape_type;
    if (shape_type == 'c') {
      Circle my_shape;
      cout << "Shape Length ? ";
      my_shape.getLgth();
      cout << "Shape Area : " << my_shape.calcArea() << endl;
    }
    else if (shape_type == 's') {
      Square my_shape;
      // --- Code repetition ---
      cout << "Shape Length ? ";
      my_shape.getLgth();
      cout << "Shape Area : " << my_shape.calcArea() << endl;
    }

    return 0;
}

Template based approach
A template is added

template <typename TShape>
// requires(std::derived_from<TShape, Shape>) // C++20 constraint
void do_job()
{
    TShape my_shape;
    std::cout << "Shape Length ?";
    my_shape.getLgth();
    std::cout << "Shape Area : " << my_shape.calcArea() << std::endl;
}

And the main is modified as follow

if (shape_type == 'c') {
    do_job<Circle>();
} else if (shape_type == 's') {
    do_job<Square>();
}

polymorphism-based approach

#include <iostream>
using namespace std;

// abstract class : Shape
// virtual function : calcArea
class Shape {
    public :
        float lgth;
        virtual ~Shape() = default;
        void getLgth() { cin >> lgth; }
        virtual float calcArea() = 0;
};

class Square : public Shape {
    public :
        float calcArea() override { return lgth * lgth; }
};

class Circle : public Shape {
    public :
        float calcArea() override { return 3.14 * lgth * lgth; }
};

int main() {
    char shape_type;
    Shape* my_shape;

    cout << "Shape Type (c/s) ? ";
    cin >> shape_type;

    if (shape_type == 'c') {
        my_shape = new Circle;
    }
    else if (shape_type == 's') {
        my_shape = new Square;
    }
    else {
        cout << "Unknown type!" << endl;
        return -1;
    }

    cout << "Shape Length ? ";
    my_shape->getLgth();
    cout << "Shape Area : " << my_shape->calcArea() << endl;

    delete my_shape;

    return 0;
}

Solution

  • There aren't any pros and cons. Templates and inheritance are distinct mechanisms solving different problems. You can sometimes force one to be used when the other is more appropriate, especially in very simple examples where you ultimately just do one thing with the object, but in general, it's best to use templates where templates are the appropriate solution, and inheritance where inheritance is the appropriate solution.

    From a more or less theoretical point of view: templates provide a common implementation for different interfaces, and inheritance provides different implementations for a common interface. For example, in the standard, std::vector is a template: std::vector<double> provides a function push_back( double ), for example, where as std::vector<int> provides push_back( int ). But both functions have the same implementation. On the other hand, the interface to std::ostream is exactly the same, although what happens to the characters being output (the implementation) is completely different for std::ofstream and std::ostringstream.

    Another important difference is that templates are resolved at compile time, but virtual functions at run-time. This means that calling a function template (or a member function of a class template) will typically be faster (but this may be offset by the need of extra code to select which function to call explicitly), and there may be more possibilities for the compiler to analyze what is going on, for better error checking or optimization. On the other hand, it means that any call site can only call one specific instance of the function; if you have multiple call sites and want to call different functions at each, templates require that you determine the type explicitly at each call site, with separate function calls for each different function.