The Life of a Programmer

Painlessly add a virtual to a large class structure

Add a new virtual to a large object hierarchy

Adding a new function to a complex class structure is arduous. A starting point is hard to find in a web of many classes and relationships. We tend to avoid large changes and do other things, like writing a blog article instead.
Here I present one technique to break the work down into simple steps. It’s a simple method, but an effective one.

My situation

I’m writing this as I need to add a clone function to the Leaf AST. The tree includes two class hierarchies with about 15 classes each, and several other minor classes. It would be too difficult, or perhaps too boring, to write all the clone functions in one pass. In this article clone is serving just as an example. The same approach works fine with any new virtual function.

A recurring problem I have with an actual clone is that it can’t be done correctly with C++. I’ll simplify what I’m doing to use a basic covariant return rather than expose the real mess I’ll end up with.

Starting with a virtual

I start by adding a clone method to one of the root classes. This is a virtual method that will be overridden by the derived classes. I don’t give this a real implementation, instead I make it cause an error. In C++ it might also seem logical to use a pure virtual function to create an abstract class. That would require me to implement it everywhere at once, which is exactly the big one-step I’m trying to avoid. In any case, my approach works even for dynamic languages.

1
2
3
4
5
6
class root {
public:
    virtual root* clone() const {
        throw "Unsupported";
    }
};

The error ensures that an unimplemented clone function will fail rather than do something wrong. A derived class must implement clone. This is of vital importance when adding new functionality to a complex system. Inevitably something will be forgotten, and silently doing the wrong thing would be a disaster. Thus we make the default behaviour to loudly announce that something is wrong.

Note: I don’t actually use throw directly; my framework has a STATE_FAIL class that uses BOOST_THROW_EXCEPTION to throw a proper error type.

A derived class needs nonetheless to clone the base — or for some other virtual function it will still have some common functionality. A protected member is created which does the actual work. Being protected helps keep it safe from unintended uses.

1
2
3
4
5
6
class root {
    ...
protected:
    //copies into this object from the provided source
    void copy( root const & o );
};

A derived class implements clone and calls copy from the base class. For consistency we might want to also define a copy operation directly in this class. It is necessary should this be used again as a root class. This approach also results in a clone function that is nearly identical in each class, perfect to replace with a simple macro.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class derived : public root {
    virtual derived* clone() const {
        auto c = new derived();
        c->copy( *this );
        return c;
    }

protected:
    void copy( derived const & o ) {
        root::copy(o);
        ...
    }
};

Note that copy is intentionally not a virtual function. I want derived implementations to hide the base definition and call it explicitly.

Wrapping up

From here the approach is simple: write a unit test and implement just one class at a time. This divides the work into many small manageable chunks and quickly exposes any weaknesses in the code. It would be awful to write a 1000 lines of code taking entirely the wrong approach.

In this case I get lucky with clone and don’t actually have to write new tests. I just added a second test path to my existing test driver. It tests both the original AST and expects the same results on the clone.

Once done it may be desirable to switch the function to a pure virtual — assuming you’re using a typed language that supports it. It’s nice to have a compiler error pointing out when a new class fails to implement it. I frequently take the approach of making temporary code at the start of a branch, and then removing, or replacing it just before merging. This simple virtual technique is one such pattern.

Please join me on Discord to discuss, or ping me on Mastadon.

Painlessly add a virtual to a large class structure

A Harmony of People. Code That Runs the World. And the Individual Behind the Keyboard.

Mailing List

Signup to my mailing list to get notified of each article I publish.

Recent Posts