简体   繁体   中英

C++ Class Inheritance Design Choice

I want to know whether one of the following two class design choices is superior in terms of performance and/or maintainability or other concerns which would favor one approach over the other.

First approach:

The base class (parent) has a private data member and one pure virtual void function. The child class(childA) passes the parameter from it's default constructor to the parent and uses the parent's getter method to print out some other value.

Second approach:

The base class (oncle) consists only of the a virual destructor and a pure virtual method. The child class (nephew) has the stores the value as a private data member and has it's own getter function to perform some calculation.

Here is some code:

#include <iostream>

class parent{
public:
    parent(int x){ _x = x; }
    virtual ~parent(){}
    virtual void calc( void ) = 0;
    int getX( void ) { return _x; }
private:
    int _x;
};

class childA: public parent{
public:
    childA(int x): parent(x){}
    void calc( void ){std::cout << "x sq: " <<    parent::getX()*parent::getX()  << std::endl;}
};


class oncle{
public:
    virtual ~oncle(){}
    virtual void calc( void ) = 0; 
};

class nephewA: public oncle{
public:
    nephewA(int x): _x(x){}
    void calc( void ){std::cout << "x sq: " << getX()*getX() << std::endl;}
    int getX( void ){ return _x; }
private:
    int _x;
};

int main(int argc, char *argv[])
{

    parent *first = new childA(3);
    parent *second = new childB(3);

    first->calc();
    second->calc();

    oncle *foo = new nephewA(3);
    oncle *bar = new nephewB(3);
    foo->calc();
    bar->calc();


    delete bar;
    delete foo;
    delete second;
    delete first;

    return 0;
}

In short: the first approach puts the emphasis on the parent and the second on the child class.

So are there any quantifiable aspects which could help me select one of the two approaches?

Performance:

In general, performance questions depend very much on compiler's optimizer and should best be addressed by an ad-hoc benchmark.

Nevertheless, for this simple example, it's obvious that the performance of both approaches should be the same:

  • Approach 1: every call to calc() is a virtual call (indirect function call). The function calls twice the parent's getX(), which is a non virtual function call (ie every calc() knows at compile time which function it has to call) fully determined at compile time.
  • Approach 2: every call to calc() is also a virtual call (indirect function call). The function calls twice the object's getX(), which is a non virtual function call, fully determined at compile time.

In addition, the compiler can inline here the non virtual functions very easily. So that optimized code will certainly not even call the function but directly use the variable.

Maintenability

You should ask yourself how much the members (x/getX()) are really bound to the class in which they are defined, and how probable it is that each element of your design could evolve.

  • Approach 1 assumes that x is managed the same way for all the children, with no exception.
    • If this is the case, this design has the advantage of central maintenance. For example, if tomorrow you'd need x to become a double, it would be straightfowrward to implement such an evolution.
    • If this basic asumption would change , all you'd have to do is to define getX() for children which should have a different behaviour. The only drawback would be that these children would still have a private x which would stay unused. If you have a couple of objects, this is not a serious issue. If you'd have millions of such objects in memory, this could be suboptimal.
  • Approach 2 assumes that every nephew is responsible for managing its own getX() This is a very flexible design, especially if there are many different rules:
    • Memory management will always be tailored to each nephew. If one nephew would for instance derivate x from another value, there wouldn't be a need to store the redundant information.
    • Flexibility can also have its drawback, especially if it doesn't correspond to reality: it could require to copy and paste the same definition of the same members accross many nephews. Such code would then be difficult to maintain (because any chagne to x or getX() would have to be replicated accross all nephews).

Conclusions:

As you see, there is no single best answer. It all depends on the real relationqship between the classes and the members.

Some examples:

  • if parent is an employee, and the child its role in a workflow, and x is the age, I would go for approach 1 (x is really bound to the parent).
  • if oncle is an abstract geometric shape, newphew the concrete shape such as square/triangle/octogon, and x is the maximum width, I would go for approach 2 (x is really bound to the nephew).

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