carsten
carsten

Reputation: 1365

interface changes: add new arguments to abstract c++ method

We have a large software framework where, in the heavy-lifting part of the code, we have an abstract base class, say

class Base {
public:
  virtual double eval() const = 0;
};

which pretty much acts as a common interface class for many other classes spread all over various sub-packages, residing in different repositories and whatnot.

class Derived1 {
public:
  virtual double eval() const override { return 1; }
}

However, there is really only one place where this function is called in our code, which is in the main loop of the core application.

 int main(int argc, char* argv[]){
    Base* x = createInstance(argv[1]); // some factory function
    while(true){
      std::cout << x->eval() << std::endl;
    }
 }

We recently discovered that it would be very useful to extend the interface such:

virtual double eval(int idx) const = 0;

Fixing the one call to this method in the core application is not the problem, but fixing the dozens of existing derived class implementations spread all over our code to accept and (in most cases) ignore this new parameter will be a nightmare.

I've thought about providing a legacy wrapper that would look like this:

double Base::eval(int idx) const { return this->eval(); } 

But this would mean that all derived classes would need to implement the non-argument version of this function to exist, even though it's supposed to be deprecated. Alternatively, we could implement the non-argument version as a part of Base, but then we would sacrifice the abstract nature of Base, because then all components would be implemented already there.

Is there any "neat" way to solve this problem, or is the only sane thing to actually contact all the subpackage developers and make them change their implementations to adhere to the new interface?

Upvotes: 1

Views: 1027

Answers (3)

Florian
Florian

Reputation: 115

You could do it like

class Base {
public:
  virtual double eval() const{}
  virtual double eval(int idx) const{
    this->eval();
  }
  virtual ~Base() = 0;
};


Base::~Base(){}

and implement either the eval() or eval(int idx) in the derived classes.

The implemented pure virtual destructor is there to ensure that the Base class cannot be instantiated. (see https://stackoverflow.com/a/14631710/2186392).

Upvotes: 1

TheOperator
TheOperator

Reputation: 6436

You could add a new abstract base class New with double eval(int idx) and deprecate the old one.

When you let one inherit the other, you can write new APIs accepting New and they stay compatible with Old.

struct New
{
    virtual ~New() {}
    virtual double eval(int idx) = 0;
};

struct Old : New
{
    virtual double eval() = 0;
    virtual double eval(int idx) { eval(); } // backwards-compatible
};

People implementing New are forced to override the unary function, while people implementing Old are forced to override the old, nullary one.

Upvotes: 6

dascandy
dascandy

Reputation: 7292

Alternatively, we could implement the non-argument version as a part of Base, but then we would sacrifice the abstract nature of Base, because then all components would be implemented already there.

You can either make one interface function as transition interface (so have both), or you can make two interfaces, in essence transitioning people to the second interface. That complicates your main loop, but does keep the classes actually abstract.

Upvotes: 1

Related Questions