I was asked to refactor some C++ code recently for the purposes of increasing unit testing coverage. The problem was that the code was tightly coupled on one compilation unit, so we had the equivalent of this:
beatles.h
class beatles { ... }
beatles.cpp
class john { ... }
class paul { ... }
class ringo { ... }
class george { ... }
...
// Implementation of beatles, john, paul, ringo and george classes ...
Because the 'internal' classes were only visible within the implementation file, they couldn't be unit tested very easily. Not only that, but the implementation file was huge and tricky to follow. I refactored the header and implementation to give something more like the following:
john.h
class john { ... }
paul.h
class paul { ... }
ringo.h
class ringo { ... }
george.h
class george { ... }
beatles.h
#include <john.h>
#include <paul.h>
#include <ringo.h>
#include <george.h>
class beatles { ... }
For simplicity, I've omitted that each one has its own separate implementation file. Naturally this is a drastic simplification but hopefully it illustrates the point.
Now we can add unit tests for the classes that previously couldn't be tested, which is great. However, for some of the operations at runtime we now see a significant performance hit, which we've concluded is due to the compiler not being able to optimise as much as it previously was, as there are absolutely no functional differences in this change.
Now, I'm aware of the benefits of loose coupling and strong cohesion. However, there comes a point where other factors such as code maintainability and testing etc. also start to come into this. I have to admit that I'm also a little uncomfortable that simply structuring our code one way or another can have such a profound, and potentially unpredictable affect on aspects such as performance.
My question is this: Is it plausible to think that the optimiser is at the root of this issue and, if so, is there a way that we can structure the code to allow the public interface for testing purposes while not throttling the optimiser?
beatles.cpp
to everywhere that includebeatles.h
, and definitions out ofbeatles.cpp
class john { void foo() {} }
are implicitly inline, whereas separate definitions likeclass john { void foo(); } void john::foo() {}
are not. Linkage refers to compilation-unit level visibility. Functions with internal linkage (likestatic
functions or functions in anonymous namespaces) and inline functions can be optimized more heavily. Optimizations across compilation units are only possible with link-time optimization (LTO).