One of the simplest techniques for maintaining large code bases is to enforce a strict hierarchy of libraries.
I first remember reading about it well over a decade ago in “Large Scale C++ Software Design” by John Lakos. Its grown in my own usage until now my own code is organised in a literal levelled folder system.
The point is that a library should only use functions lower down than itself, so there are clear path from higher level functionality toward code with less dependencies. It also avoids cycles where X depends on Y which depends on X, it can’t happen as Y can’t depend on X.
So my libs structure is divided into levels (except my level zero libraries which live in the root of the libs structure).
Then level1 contains
And so on, a library in level 1 can only access core and binify, a level 2 library has access binny, input and math as well as core and binify.
I use cmake’s parent scope feature to inject a levels set of libraries without the levelX part into the include paths further down the chain.
This results in my app directory (where the actual program source code live) being able to access any of the libraries via library/foo.h even though it might be physically located in level2/library/foo.h. The same is true for the libraries themselves, though obviously they can only access libraries of a lower level. This makes it easy to adjust a library’s level as needed (its simple a physical move and change of 2 lines in cmakelists.txt files, I’m tempted to automate the last step so it automatically picks up movements of libraries).
I have a strict policy of include files via a #include “library/include.h” syntax even when the library part isn’t strictly necessary, this make it more visible and to me means I can treat the include path as a form of name spacing. I don’t feel bad about having the same named file in separate folders, as it obvious which is being used by the library prefix.
So for example my low level render system is a level 3 and lives in level3/render. My Vulkan implementation of that system lives in level 4 at level4/vulkan. My mid level render system lives in level4/midrender as it derives off render, the fact that behind the scenes potentially uses level4/vulkan doesn’t matter. It depends only on level 3 functionality, so its a level 4 library. My namespace follows the include path fairly closely (some differences due to case choices). The namespace are Render, Vulkan and MidRender (I choose camel case to separate from the standard library all lower case name spaces). There are texture.h files in both render and vulkan but are always referred to as “render/texture.h” or “vulkan/texture.h” so easy to tell which was intended.
Its not much work to implement and really helps as things grow, its usually fairly easy to tell where new functionality should go and stops any accidental breaking of the architecture.