This is a topic I have visited a few times over the years:
2016, 2016 on SDL2, 2019, 2023 on control block, and I visited it professionally on the now cancelled AAA MMO Game Engine with Zenimax Online; the specific pattern within which came from my original 2016 post, just with a few tweaks and expansions over the years.
Its just one of those things which comes up time and again in the C++ space, much like my crusade against return …
This is a topic I have visited a few times over the years:
2016, 2016 on SDL2, 2019, 2023 on control block, and I visited it professionally on the now cancelled AAA MMO Game Engine with Zenimax Online; the specific pattern within which came from my original 2016 post, just with a few tweaks and expansions over the years.
Its just one of those things which comes up time and again in the C++ space, much like my crusade against return early in performance code... cough.
And it came up again just this week, where given a coding test I immediately did not like seeing raw pointers being used, so quickly refactored it to use a factory create pattern, except I was in a rush and didn’t have the luxury of a whole game engine backing me up.
So denuded of even my own post I quickly used a different approach, so use a hidden (private) member struct in the constructor. The constructor remains public so std::make_shared or std::make_unique can access them without hinderance from the static create factory member function, but that a regular user can not use a flat “operator new” as they can not access the private struct.... Sounds complex, but it’s really not, and you will even find this exact pattern on CPP Reference sites across the web, let us quickly output an example (and I’ll assume you’ve taken a look at my prior post from 2016 above for the actual “shared pointer friendship” I would prefer).
struct Thing
{
private:
struct HiddenInternal {}; // Just an empty struct
public: using Ptr = std::shared_ptr<Thing>;
static Ptr Create() { return std::make_shared<Thing>(HiddenInternal{}); }
explicit Thing([[maybe_unused]] HiddenInternal internal)
{
}
};
And so with this code we can only get a Ptr instance here from the static Create call.
Now I’ve set the scene, what am I asking myself? I’m asking what is the compile time and what is the runtime effect of these two patterns?
Compiletime my suspicions are that the shared_pointer friendship stuff in my original post is more weight to carry, there’s a macro in my more complex implementations, so that’s pre-processor overhead, there’s expansion of the code at compile time.
But there’s also this HiddenInternal in this new, simpler, implementation based solve; I believe there will even be compiler ellision of the HiddenInternal as its empty and does nothing; I just need to observe and understand this topic better.
Lets start with Compiler Explorer and my first confirmative discovery, just as I suspected, the compiler is very smart smart and it will easily see a trivial class such as mine above, or one with a single member, and fold it away to nothing in compiler ellision.
**And this is a great thing! **It proves at zero runtime overhead whilst we have code fully communicative of the intent, self documenting code is a wonderful thing, and this is an interesting and eye catching pattern in the code.... Such that I believe even a new set of eyes meeting it would appreciate there is something special intended and they would take care to use the class properly, avoiding memory leaks for us along the way.
Good code health is as important as the final result, I especially take pride in code which can run a long duration. I’ve always worked on long lived project timelines.
So HiddenInternal can result in no overhead when the class is trivial, what about when the class is complex?
The first thing I notice is that the factory creation function itself generates no code, yet we benefit from its intent at compile time and in the usage. There is of course the control block creation for the shared pointer in this instance and I hold that in my mind, but the cost to pay back in intent of the code far outweighs the control block.
The constructor itself is interesting....
Removing the HiddenInternal gives no difference, so the compile has still removed the empty class, but enforces it’s use in the code... This is a total win-win... The cost at compile time, one compile time ellision in one translation unit which is neglegable.
The runtime overhead zero.
What about the preprocessor and friendship.... The friend keyword itself is zero cost, the wrapping of it in a macro is therefore arguably a different conversation to have. Resolving a friendship however is not so cost effective, the friend keyword adds a symbol, typically with a cost of O(1).
The look up from any other resolve is therefore either going to be O(1) or O(log N) where N is the number of other appearances of the name, this look up is going to be the compile time cost. In practical terms this is going to be a very small duration, perhaps even below measurable in the pantheon that is a build across multiple cores.
A friendship link itself however costs nothing in code generation time, so we do not hold up the compile per se, we do however add a cost in the symbol table build.
Once something is in the symbol table it will result in complexity when refactoring code, potentially more rebuilds if the friendships, or classes within change.
In conclusion, if I have the option to set up a friendship to the underlying control block (see 2016) I would, but equally to keep the local complexity low I would also consider the Hidden private member class as a “trick”.