Examples suffer from being oversimplified. For example, the components you list are (in real life) not only attached to the car, but also to the adjacent components themselves. In programming terms, Engine
would have a direct reference to Transmission
, and so on.
A better analogy here would be a desktop PC. All components are plugged into the motherboard. You don't connect your CPU to your graphics card or your HDD directly, they all get connected to the same motherboard which does the routing between components. This reflects your code example, as Car
acts as the mediator between all of its components.
It causes Car to (indirectly) contain a bunch of pointers to itself
You're ignoring the basic encapsulation principle here, because the internal component logic is not something that Car
cares about, and therefore the consideration you mention is not relevant. Car
does not contain references to itself. It contains components. Those components just happen to have a symmetric reference to the car they belong to.
These two facts should only be considered individually, not as one big conclusion, because encapsulation specifically asks that you only judge a class by the class itself, not other classes.
forcing me to declare copy and move constructors manually, creating unnecessary space for bugs
I'm no C++ dev so I'll take your statement at state value, but if it is necessary that the component needs a reference to their car, and doing so inherently means declaring copy and move constructors, then this implementation is not "unnecessary" as you call it.
Also as a minor issue this needlessly inflates the size of Car class.
It doesn't. It increases the size of objects of the component classes. It's a recurring trend in your question that you think of your multiple classes as if they're one big class, but they're not. Encapsulation incentivizes you to think about individual classes, not groups of them, because it becomes very easy to muddy lines you should not be muddying.
I'm not saying that memory footprint doesn't matter, but you're inverting the order of operations here. The feature requirement is more important than the space optimization, so if this is what you need, then you're not wasting the extra space it requires.
In a magical perfect-c++-land I'd be able to create some sort of a namespace inside the Car class, adding a eg. Engine:: prefix to all names belonging to engine, but keeping the this pointer pointing to the top level Car object.
This violates the basic premise of encapsulation. You're effectively breaking down the borders between your individual classes just to avoid storing a pointer. This is the equivalent of not putting a front door in your house because the people you live with would rather not put a key in their pockets. The end really does not justify the means here.
afterwards I can use this from the outside as
car.engine.do_stuff();
You're starting to violate the Law of Demeter here. Based on your class definition, a component should not have any knowledge of what components a Car
has. At best, it knows that it itself belongs to a car, but whether that car also has a radio is something the transmission should not occupy itself with.
The problem is that when you do this, you start painting yourself into a corner. Now, your transmission only works if the car has an engine. But, just for the sake of example, what if this car is pedal-operated? It doesn't have an engine, but it still needs a transmission. But the way you built your transmission component, it's actually not usable anymore.
What do you use in cases like this?
I prefer symmetrical references like you already used. Pointers take more effort in C++ than they do in e.g. C#, but that's the nature of the beast. C++ is not the easiest language to work with. With great control comes the need for significant amounts of handholding and pedantry.
The principle of OOP does not change just because your programming language changes, even if C++ is more complex than other languages.
Are there some alternative workarounds?
You don't need the symmetric relationship if the communication only works one way.
Rather than "engine needing to send its rpm to transmission", think of it like "car asks engine for its RPM, and car then gives the RPM value to transmission".
Now, only car needs a reference to its components, not the other way around, and thus you don't need to pass the car reference to the components. Whether that works for your use case very much depends on your specific use case.
Engine_
prefix for all your engine-related named. Less than perfect, as there is no mechanism to skip that prefix in selected contexts.