Adding a parameter with a default value to a virtual method

In a multi-tier development context, where the core library and client code are controlled by completely unrelated development teams (many client development teams), what is the most efficient way to extend the base class interface by adding a new parameter with a default value to the a method?

Conceptually I need to replace (in the main library) this old code:

struct Base
{
    virtual void foo() {}
    vitrual ~Base() {}
}

      

with this new code:

struct Base
{
    virtual void foo(bool b = true) {}
    vitrual ~Base() {}
}

      

The problem is that this will silently break the client code, for example:

struct Derived: public Base
{
    void foo() {}
}

int main()
{
    Derived d;
    Base &b = d;
    b.foo();
}

      

One solution would be to have both methods, for example:

struct Base
{
    virtual void foo(bool b) {}
    virtual void foo() {foo(true);}
    vitrual ~Base() {}
}

      

This adds an unnecessary technique that is not a sustainable approach to library maintenance (interface bloat, maintenance cost, testing, documentation, etc.). Of course, the old method may be deprecated, but that would mean that new client code must always specify a boolean parameter.

another solution might be to provide a new version of the base class:

struct BaseV2: public Base
{
    virtual void foo(bool b = true) {/* delegate impl. */ }
}

      

This adds an unnecessary class, but at least the deprecation can be handled conveniently on the client side.

What are the other options? What can be done to make it easier to introduce such trivial interface changes into the main library?

+3


source to share


2 answers


Several things can help:

1) Using the keyword override

will give a compiler warning whenever the method is now shadowing rather than overriding the base class method. eg:.

struct Derived: public Base
{
    void foo() override {} // Warns when Base::foo changes
};

      

Unfortunately, this is something you should rely on your users, not what you can provide. If your users are in enough pain, they can go for it.

2) Separate your class interface from its implementation - in this case, the implementation is virtual and ideally private. eg.

struct Base {
    void foo() { fooImpl(); }
private:
    virtual void fooImpl() = 0; // Or provide a default implementation
};

struct Derived : public Base {
private:
    void fooImpl() override { ... } 
};

      



In this case, you can add the default argument to foo()

without breaking anything, and then decide what to do with other users of your codebase.

If you decide that you absolutely need to pass the parameter to the client executors fooImpl()

without keeping the legacy version, you can change its signature. With a pure virtual then the compiler will stop any class instances where overriding no longer occurs and you don't get a quietly broken compiler. Pros: No bad builds, Cons: Works for some of your users, even if they don't care about the new features.

Alternatively, if you decide that the behavior of your class should be delegated to another function as a result of a parameter, eg. fooImpl2(...)

, then in Base::foo

you can check if this variable is the default and call fooImpl

or if necessary fooImpl2

. fooImpl2

don't just take a redundant copy of the bool parameter, of course; your delegation code can call it with completely different parameters, if your implementation foo

can decide what to do with the old method signature and your new parameter.

Going down a route fooImpl2

you can choose to provide a default implementation (pro: every code compiles and runs effortlessly; con: you must provide a sensible default implementation) or make this pure virtual (pro: easier for you; con: all other code breaks, even if they don't want to implement your new interface).

Another benefit of this approach is that it is now known that all users of your frontend are logged in using a method you control, so authentication / logging / general behavior / pre-and post-consistency checks can be done in one place instead of for everyone to half bake their own thing.

3) Maybe consider mixins , depending on what your new default is for. On the other hand, this approach provides the maximum flexibility for you and your users in combining methods, creating new ones, and not having to write new code when nothing has changed. On the other hand, the error messages will be incomprehensible, and if there are people in the organization who are not very familiar with template programming, things can go wrong.

+3


source


This adds an unnecessary technique that is not a sustainable approach to library maintenance (interface bloat, maintenance cost, testing, documentation, etc.).

Therefore, software with dependent client code, which cannot be practically cleaned up as changes are made, go through minor cruft add cycles and then clean up / new version, which breaks backward compatibility.



When this is simply completely unacceptable, some hideous alternatives are used - like functions that accept containers, which can subsequently carry arbitrary parameters that are decoded at runtime. If you are desperate, sleep on it.

+1


source







All Articles