The 4 Pillars of OOP in Production C++
In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.
When callers call getters (account.getBalance()), make decisions, and then call setters (account.setBalance(...)), business logic is scattered across clients and class invariants are easily violated. Under 'Tell, Don't Ask', clients tell the object what goal to achieve (account.withdraw(500)). The object encapsulates both data and business rules, ensuring invariants remain intact in one authoritative location.
Compile-time polymorphism (templates, CRTP) generates separate specialized functions during compilation (monomorphization). The compiler inlines calls and eliminates function call overhead entirely (zero pointer indirection). Runtime polymorphism uses virtual functions and requires two pointer dereferences: loading the object's vptr, and indexing into the vtable to fetch the function pointer. This prevents inlining (unless devirtualized) and can cause instruction cache misses.
There are only two rare technical reasons in modern C++: (1) Empty Base Optimization (EBO): inheriting from a stateless base class (sizeof == 0) takes 0 bytes in Derived, whereas a member variable would take at least 1 byte (though C++20 [[no_unique_address]] largely supersedes this); and (2) When Derived must override a protected virtual function of Base or access protected members while keeping Base hidden from outside users.
Interviewer's lens
Interviewers frequently open OOP discussions with the Four Pillars to test whether you think like an application user or a systems engineer. Anyone can recite textbook definitions, but senior C++ engineers reason about memory layouts, compiler checks, and state boundaries:
- Encapsulation: Enforcing strict class invariants with 'Tell, Don't Ask' instead of blind getters and setters. Access specifiers (
public/private) exist purely at compile time with 0-byte runtime overhead. - Abstraction: Defining strict contracts via pure virtual interfaces (
= 0) so callers depend on promises, not concrete wiring. - Inheritance vs. Subtyping: Distinguishing behavioral subtyping (
publicinheritance adhering to the Liskov Substitution Principle) from private implementation reuse. - Composition over Inheritance: Combining small, focused components like LEGO bricks to avoid the Fragile Base Class problem and combinatorial class explosion.
Under the hood & memory layout
In C++, OOP concepts are not abstract philosophyβthey map directly to physical memory layouts, compiler symbol tables, and CPU instruction pipelines:
- Encapsulation (Compile-time door lock):
public,protected, andprivateare frontend compiler visibility checks. They rearrange no bytes in memory and cost zero runtime CPU cycles or memory overhead. - Abstraction (Contract without code): Pure virtual methods (
= 0) leave an unassigned slot or a crash trap (__cxa_pure_virtual) in the class function table. The compiler strictly forbids instantiating incomplete contracts. - Inheritance (Physical memory prefix): Single inheritance places the base subobject directly at byte offset 0 of the derived object. Casting a
Derived*to aBase*is a zero-cost, zero-offset pointer alias. - Polymorphism (Static vs. Dynamic dispatch):
- Static: Overloads and templates resolve during compilation (monomorphization) with direct inlined machine code and zero pointer hops.
- Dynamic: Virtual functions resolve at runtime through an 8-byte pointer (
__vptr) that hops to a function table (vtable) in read-only memory (.rodata).
- Composition (Inline packing): Member subobjects sit directly inside the parent's memory box. This packs data contiguously into 64-byte CPU cache lines, maximizing hardware prefetcher efficiency.
0 bytes runtime overhead. Access specifiers (public:, private:) are compile-time access controls. In modern C++, non-static data members with the same access control are laid out sequentially in order of declaration.
An abstract class with pure virtual methods generates a vtable structure where unoverridden entries point to __cxa_pure_virtual (which aborts if called). Instantiating an abstract class directly is rejected by the compiler.
Under single inheritance, Derived layout begins with the Base subobject (including its vptr at offset 0), followed immediately by derived-specific members. A Derived* to Base* upcast compiles to a zero-offset no-op pointer cast.
Direct member composition (struct Car { Engine e; };) places Engine inline inside Car memory with zero heap indirection, maximizing CPU cache line utilization (64 bytes per cache line).
Core concepts & syntax traps
Encapsulation: Class Invariants, Access Specifiers & 'Tell, Don't Ask'
Encapsulation: Class Invariants, Access Specifiers & 'Tell, Don't Ask'
Think of an object as a bank vault with an internal guard, not an open drawer with a label. If you expose raw getters and setters for every variable, you have an awkward global struct with extra typing. True encapsulation guarantees that an object can never enter an invalid state:
- Guard the Gate (Constructors): Validate state immediately upon creation. If input is invalid, fail early by throwing an exception.
- Defend Transitions (Mutators): Member functions must reject operations that violate business rules (e.g., withdrawing more money than the current balance).
- 'Tell, Don't Ask': Instead of pulling data out (
getBalance()), calculating outside, and pushing it back (setBalance(b - 100)), tell the object directly what task to perform (account.withdraw(100)). The object controls its own rules in one place.
// POOR: Anemic domain model with raw getters/setters
class AnemicAccount {
double balance_;
public:
double getBalance() const { return balance_; }
void setBalance(double b) { balance_ = b; } // Anyone can set balance to -999999!
};
// PRODUCTION: Encapsulated invariants with 'Tell, Don't Ask'
class BankAccount {
int64_t balanceCents_{0};
public:
explicit BankAccount(int64_t initialCents) : balanceCents_(initialCents) {
if (initialCents < 0) throw std::invalid_argument("Negative initial balance");
}
bool withdraw(int64_t cents) noexcept {
if (cents <= 0 || cents > balanceCents_) return false; // Invariant preserved!
balanceCents_ -= cents;
return true;
}
};Encapsulation guarantees that internal class invariants cannot be violated by external callers regardless of call sequence.
Abstraction: Pure Virtual Interfaces (= 0) & Separation of Concerns
Abstraction: Pure Virtual Interfaces (= 0) & Separation of Concerns
Abstraction is a standard electrical wall outlet: appliances only care about the shape of the plug and voltage, not whether power comes from solar, coal, or nuclear.
In C++, an abstract interface enforces this separation mechanically:
- The Contract (
= 0): Marking a virtual function pure (= 0) tells the compiler: 'This class has no implementation for this function.' Concrete child classes must implement it before an instance can be created. - Safe Deletion (
virtual ~Interface() = default;): An interface must always provide a virtual destructor so deleting via an interface pointer cleans up child resources properly. - Caller Decoupling: Client code depends only on interface references (
const IPaymentGateway&). You can swap payment vendors or mock test engines without touching a single line of business logic.
class ITelemetrySink {
public:
virtual ~ITelemetrySink() = default; // REQUIRED for safe polymorphic destruction
virtual void emitMetric(std::string_view metric, double val) = 0;
virtual void flush() = 0;
};
// Client depends ONLY on the abstraction
void runBatchJob(ITelemetrySink& sink) {
sink.emitMetric("batch.started", 1.0);
// ... work ...
sink.flush();
}Always program to an interface (abstract base class), not a concrete implementation, to decouple modules.
Inheritance Modes: public (Is-A) vs protected vs private (Implemented-In-Terms-Of)
Inheritance Modes: public (Is-A) vs protected vs private (Implemented-In-Terms-Of)
C++ offers three distinct access modes when inheriting from a base class:
publicInheritance ('Is-A'): Strict behavioral subtyping adhering to the Liskov Substitution Principle (LSP). ADogis aMammal. Anywhere aBase*is expected, aDerived*can be passed implicitly.protectedInheritance ('Family-Secret-Is-A'): Base public and protected members becomeprotectedinside Derived. The outside world cannot castDerived*toBase*, but further subclasses down the chain still can.privateInheritance ('Implemented-In-Terms-Of'): Base members become private implementation details of Derived. Outside code cannot upcastDerived*toBase*.
Modern Rule: Favor Composition over private inheritance. Storing an engine object as a private member gives cleaner lifetime management, avoids accidental name collisions, and lets you swap engine types at runtime.
class Engine { public: void fireSpark() {} };
// PUBLIC: Car IS-A Vehicle
class Car : public Vehicle { /* ... */ };
// PRIVATE: Car is implemented in terms of Engine (rarely justified over composition)
class RigidCar : private Engine {
void drive() { fireSpark(); } // OK internally, hidden externally
};
// COMPOSITION (Preferred): Car HAS-AN Engine
class CleanCar {
Engine engine_;
public:
void drive() { engine_.fireSpark(); }
};Use public inheritance strictly for polymorphic subtyping (Is-A); use composition instead of private inheritance for implementation reuse.
Polymorphism: Compile-Time (Templates/CRTP) vs Runtime (Virtual Dispatch)
Polymorphism: Compile-Time (Templates/CRTP) vs Runtime (Virtual Dispatch)
Polymorphism allows one interface to control multiple underlying behaviors. C++ divides this into two distinct mechanisms:
- Compile-Time (Static) Polymorphism:
- Mechanic: Resolved by the compiler using function templates, overloads, or CRTP (Curiously Recurring Template Pattern).
- Cost: Zero runtime overhead. The compiler generates specialized machine code for each concrete type, enabling aggressive inlining and loop vectorization.
- Runtime (Dynamic) Polymorphism:
- Mechanic: Resolved at runtime through pointers/references to base classes using
virtualfunctions and lookup tables (vtables). - Cost: Exactly two pointer indirections per call. Use dynamic dispatch when types cannot be predicted ahead of time, such as loading plugins from a disk config at runtime.
- Mechanic: Resolved at runtime through pointers/references to base classes using
// Static Polymorphism (CRTP): Zero runtime dispatch overhead
template <typename Derived>
class SerializerBase {
public:
void serialize() {
static_cast<Derived*>(this)->writeBytes();
}
};
class BinarySerializer : public SerializerBase<BinarySerializer> {
public:
void writeBytes() { /* inlined by compiler */ }
};Choose compile-time polymorphism for maximum throughput and predictable types; choose runtime virtual polymorphism when types vary dynamically.
Composition over Inheritance & The Fragile Base Class Trap
Composition over Inheritance & The Fragile Base Class Trap
Treating inheritance as the default tool for code reuse leads to two major architectural traps:
- The Fragile Base Class Trap: Inheritance tightly glues derived classes to base implementation details. When an engineer changes a single helper method in
Base, subclasses ten levels deep silently malfunction or violate state invariants. - Combinatorial Class Explosion: If a
Vehiclehas 3 engine types, 3 tax rules, and 2 navigation modes, pure inheritance requires $3 Γ 3 Γ 2 = 18$ separate subclasses (ElectricTaxedAutonomousCar, etc.).
The Fix (Composition): Assemble classes from small, independent strategy parts (like snap-together modules). A Vehicle holds std::unique_ptr handles to an IEngine, ITaxPolicy, and INavigator. The class count drops from 18 subclasses down to $3 + 3 + 2 = 8$ standalone components, and you can hot-swap strategies at runtime.
// COMBINATORIAL EXPLOSION: Every permutation requires a new class!
// class ElectricTaxedHumanCar : public Car { ... };
// class GasUntaxedAutonomousTruck : public Truck { ... };
// COMPOSITION: Orthogonal concerns are injected strategies
class ComposedVehicle {
std::unique_ptr<IEngine> engine_;
std::unique_ptr<ITaxPolicy> taxPolicy_;
public:
ComposedVehicle(std::unique_ptr<IEngine> e, std::unique_ptr<ITaxPolicy> t)
: engine_(std::move(e)), taxPolicy_(std::move(t)) {}
};Favor object composition over class inheritance: assemble features by combining small, focused interfaces rather than subclassing.
Unlock Live Implementation & Interview Scenarios
The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.