In embedded systems, hardware components must shut down in a strict order.
If the shutdown sequence is incorrect, peripherals may hang, buses may lock, or clocks may remain enabled unnecessarily.
You are given three system components:
Your task is to design a system where:
main() does not manually start or stop any componentThis must be achieved only using composition and destructors (RAII), without explicit cleanup calls.
What You Must Do
SystemControllerSystemController must own:ClockBusDriverBus uses ClockDriver uses Busmain()Program Flow
SystemController object inside a local scopeInput
Two integers provided via standard input:
Output
The output must match exactly, including order and formatting:
Clock enabled
Bus initialized
Driver started
Bus write: <addr> <value>
Driver stopped
Bus stopped
Clock disabled
Constraints
Clock, Bus, and Driver must NOT be created in main()SystemController must be the only owner of all components
These three concepts form the backbone of robust C++ architecture.
Car has an Engine, it is not an Engine.1. Composition (Building Blocks)
Composition allows you to swap parts easily and keeps dependencies clear.
class PWM {
public:
void set(int duty) { /* ... */ }
};
class Encoder {
public:
int read() { /* ... */ }
};
// Composition: MotorController "Has-a" PWM and "Has-a" Encoder
class MotorController {
PWM pwm_driver; // Sub-component 1
Encoder enc_driver; // Sub-component 2
public:
void set_speed(int speed) {
int current = enc_driver.read(); // Delegate work
pwm_driver.set(speed);
}
};2. RAII (The Scope Guard)
The most common embedded use case is managing interrupts or mutexes.
class ScopedIntLock {
public:
ScopedIntLock() { __disable_irq(); } // Acquire
~ScopedIntLock() { __enable_irq(); } // Release
};
void critical_function() {
ScopedIntLock lock; // Interrupts disabled HERE
// ... do critical work ...
if (error) return; // Safe! Destructor re-enables interrupts automatically.
} // Destructor runs here naturally.| Ownership Type | Concept | C++ Tool |
|---|---|---|
| Owner | "I control this life. When I die, it dies." | std::unique_ptr or Member Variable. |
| Shared Owner | "We keep this alive as long as one of us needs it." | std::shared_ptr. |
| Observer | "I just look at it. I hope it's still there." | Raw Pointer (T*) or Reference (T&). |
1. Composition over Inheritance
Embedded hierarchies can get messy ("Is a SPI driver a Stream? Is it a Bus?").
Composition is cleaner. A Sensor class shouldn't inherit from I2C. It should contain a pointer/reference to an I2C driver. This allows you to easily swap the I2C driver for a SPI driver without rewriting the Sensor class.
2. Leak-Free Error Handling
In C, if an error occurs in the middle of a function, you must remember to free() memory or unlock() mutexes before every return.
With RAII, you just return. The compiler inserts the cleanup code for you (Stack Unwinding), making firmware crash-proof.
3. Clear Lifetimes
By using Composition and RAII, you avoid "dangling pointers" where a driver tries to access a hardware object that has already been de-initialized. If the parent object (Robot) dies, its children (Motors) are destroyed automatically and in the correct order.
| Pitfall | Details |
|---|---|
| ❌ Deep Inheritance | Avoid creating 5-layer deep class hierarchies (Driver -> Serial -> UART -> STM32_UART). It increases code size (v-tables) and makes debugging a nightmare. Prefer Composition. |
| ❌ Raw Pointers as Owners | Never use a raw pointer (
|
| ❌ Circular Dependency | If Object A owns B, and Object B owns A (via shared_ptr), they will keep each other alive forever (Memory Leak). Use weak_ptr or raw references to break the cycle. |
| ✅ Dependency Injection | Pass the "Sub-components" into the Constructor rather than creating them inside.
|