1

Let's say I want to make a class for a car. A car has an engine, transmission, steering, radio, ... Each of these parts have its own internal state, so in C++ it makes sense to use composition like this:

class Car {
public:
   Engine engine;
   Transmission transmission;
   Steering steering;
   Radio radio;
};

On the other hand these sub-objects also need to somehow cooperate (eg. engine needs to send its RPM info to transmission), so I'd construct each of the components with a pointer to the car object:

class Engine {
public:
    Engine(Car*);

    void do_stuff() { car->transmission->set_engine_rpm(123); }
private:
    Car* car;
}

afterwards I can use this from the outside as

Car car;
car.engine.do_stuff();

Generally this works ok, creating a (in my opinion) simple and ergonomic interface for Car and nicely separating the component states, but is not optimal. It causes Car to (indirectly) contain a bunch of pointers to itself, forcing me to declare copy and move constructors manually, creating unnecessary space for bugs. Also as a minor issue this needlessly inflates the size of Car class.

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.

Are there some alternative workarounds? What do you use in cases like this?

Edit:

The motivation for this question came from writing a top level signleton object on a microcontroller that provides hardware abstraction for the rest of the code. This means I am not that concerned with flexibility of this code, there will never be a need for car with two engines and no transmission, or for multiple cars with different engines in this project. Also being on a microcontroller this makes me think a bit more about performance unnecessary memory use and dynamic allocations, even though currently I'm not close to the limits of my environment yet.

And yes, there will be a class SimulatedCar (at least two, in fact), that allow me to simulate and test the rest of the code.

1
  • In the current, less-than-perfect C++ land, you can use an Engine_ prefix for all your engine-related named. Less than perfect, as there is no mechanism to skip that prefix in selected contexts. May 17, 2021 at 13:10

4 Answers 4

1

tl;dr

How can composed sub-objects access the parent object?

They don't need to, either in your example problem, or in your motivating problem.


You seem to have chosen an example which is a poor illustration of your real problem, and then fixated on a poor approach to the example. Nothing in either requires member objects to access their parent.

The Problem as Stated

... these sub-objects also need to somehow cooperate (eg. engine needs to send its RPM info to transmission), so I'd construct each of the components with a pointer to the car object

No, take a step back. If an engine needs to send its RPM info to a transmission ... then an Engine needs some link to a Transmission. Nowhere in that sentence does the word "car" appear. There's no reason to assume you need a Car* to do this.

If you're thinking about this as an OO representation of the real world (which is often problematic), then it should also be possible to run an engine on a test bench. Equivalently, you should be able to test an Engine without having to assemble a complete Car around it, only mocking the components needed for the test.

So, it should be sufficient for the Transmission to implement an input interface (call it a Crankshaft or something), and for an Engine to send its output to this interface. Neither of them needs to know about a Car, and you can easily set up a test bench with a MockCrankshaft which just provides a readout of the RPM.

Later, when you want to add support for torque (and say some way to detect stalls), those would also go via the crankshaft interface.

The Motivating Problem

The motivation for this question came from writing a top level singleton object on a microcontroller that provides hardware abstraction ... being on a microcontroller this makes me think a bit more about performance unnecessary memory use and dynamic allocations ...

Right, so all your components probably do need to know about your HAL ... but it would be really strange to make the HAL the top-level container as well, and it doesn't explain why you think communication between components needs to go via the HAL either.

If you really want to squeeze out unnecessary memory use then templating everything on the HAL is probably easiest. Templates can lead to code bloat in some situations, but here you're only building a single instantiation for a given target.

// whatever types and operations you need to abstract:
struct Platform1 {
  using native_int = std::int16_t;
  using address = std::uint16_t;

  native_int IO_readwrite(address, native_int);
};
struct Platform2 {
  using native_int = std::int32_t;
  using address = std::uint16_t;

  static native_int IO_readwrite(address, native_int);
};

// your application can use HAL::IO_readwrite etc. for abstracted features
template <typename HAL>
struct Composer
{
  ComponentA<HAL> a;    // aka. engine
  ComponentB<HAL> b;    // aka. transmission
  ConnectorAB<HAL> ab;  // aka. crankshaft

  Composer() : ab(a,b)
  {}
};
4

In short: Getting it done smoothly in C++

You've opted for embedded object composition. The advantage is that Engine is constructed together with the Car, and gets destroyed with it. Simple, and effective !

But there are painful inconveniences:

  • you need raw pointers and these require you to abide by the rule of 3 (or even the rule of 5): copy constructor, assignment operator, destructor. You already figured out this issue. That's some boilerplate code; the price to pay for the ease of construction.

  • you cannot benefit from polymorphism: you may not replace at runtime an Engine with a more powerful SpecializedEngine. If you would, you'd get object slicing. That's the cost of hardwiring at construction.

There are two easy modern alternatives:

  • Use composition via shared_ptr<Engine> : Ok, you'll need to assemble the car at construction. But each copy of a Car would reuse the same engine, unless you'd decide it to replace it.
  • Use composition via a unique_ptr<Engine> : unique pointers cannot be copied and this would prevent cars from getting copied.

Some more philosophical considerations (optional)

These are unrelated of your pointer issue in C++, but I feel compelled to address them as well. Ignore if not interested.

How is an engine related to a car?

In the mechanical world, an Engine does not have to know the Car in which it is used, and the Car does not have to know the internals of the Engine. Both are connected via well-defined interfaces:

  • The engine's internal crankshaft is well encapsulated. Only a tiny part is public, and provides motion to an external flywheel that transmits the produced energy to the rest of the car.
  • The combustion engine also requires incoming gazoline, as well as oil. There are ingoing pipes for that.

That's a piece of art of engineering: a perfectly decoupled design, with independent components and separation of concerns. You can replace any part with another, provided it complies exactly with the interface.

Legal disclaimer: I'm not a mecanician. Mechanics is awfully complex. For mechanical advice, please consult a qualified mechanician in your jurisdiction ;-)

Decouple, decouple, and decouple again

If your Engine needs to know about the Car, you have strongly coupled these two classes. You no longer can mount your V6 engine on a high-end lawn mower or a fishing boat, even if those would provide the correct interface on their side!

Why? Because your Car's implicit interface does not comply with the Interface Segregation Principle. Engine should not know about Car. It should only know about Transmission.

But, I'll end here with mechanical analogy (analogy to the real world, will not necessarily lead to the best design) to focus on the decoupling. Some possible approaches:

  • make Car a mediator: each component just knows the Car, and the Car orchestrates between the components: Engine tells Car that it made some motion, and Car will know that this must be told to the Transmission. Advantage: Car derives from abstract MechanicalMediator, and tomorrow, LawnMower can also derives from MechanicalMediator and work with the Engine.

  • make Car and other relevant components observers of the Engine.

  • go for an event-based design, and provide Car with an event-queue to which all its components can register to consume events of interest.

The “dreamworld” that you describe, where every car component would be just some namespace and this always refers to the car, would not allow for real independent components. It would be a nightmare for maintenance.

2

If the copy and move constructors bother you, you could insist on cars being non-copyable (I don't know whether that's possible in C++) so a new car must always be constructed from scratch, just like in the real world.

A somewhat more elegant solution is to pass down references to the containing object and possibly other related objects when calling methods of the inner objects, so the doStuff() method in Engine would have a Car *car parameter. The downside of this is that you either pass references to every Engine method, or the fact that some methods need the Car object must be encoded in the external interface of all car components, which might create a maintenance burden.

1
  • 1
    Agreed. I would just note that it's often a good idea in this scenario to pass in an abstract type that only references the things that component needs.
    – JimmyJames
    May 17, 2021 at 13:29
1

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.

3
  • A more realistic example for your engine -> pedals example would be a something like a Tesla which has no engine or transmission and 2 motors.
    – JimmyJames
    May 17, 2021 at 13:38
  • @JimmyJames: The point of the example was to retain one of the existing componenets from the original car isntead of chucking them both out, since I was focusing on reusability.
    – Flater
    May 17, 2021 at 14:12
  • I get it but the issue with building 'inside-out' objects is roughly similar in that the code that 'drives' the car becomes non-reusable. I find sometimes people get bogged down in whether a hypothetical example is realistic and miss the point.
    – JimmyJames
    May 17, 2021 at 14:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.