C++ Advanced
A Complete 6-Chapter Programming Course
Table of Contents
- Multiple Inheritance & Virtual Inheritance
- Templates, Deeper
- The Rule of Five & Copy/Move Semantics In Depth
- Concurrency in Modern C++
- Undefined Behavior in C++
- Capstone: Building a Small Project
Multiple Inheritance & Virtual Inheritance
Course 3 goes deeper into C++'s own mechanics, starting with something neither of this course's two comparison languages support in the same form.
Multiple Inheritance
A class can inherit from more than one base class at once โ something neither C's plain structs (c2-1, no inheritance concept at all) nor Rust's traits support in this exact form.
The Diamond Problem
The classic scenario: class D inherits from both B and C, and both B and C themselves inherit from a common base A. Without intervention, D ends up with two separate copies of A's data โ one via each path โ genuinely ambiguous which copy a reference to an inherited A member refers to, producing a real compile error unless explicitly disambiguated.
A Worked Example
virtual Inheritance โ The Fix
With both paths declared virtual, Duck gets exactly one shared Animal sub-object, resolved correctly. Under the hood, a hidden pointer/offset table tracks the shared base's actual location โ genuinely comparable in spirit to cpp1-7's own vtable: hidden compiler machinery solving an ambiguity problem.
Why This Is Genuinely Complex and Often Avoided
Honestly: multiple inheritance combined with virtual inheritance is widely considered one of C++'s more error-prone, rarely-actually-needed corners. Most style guides recommend avoiding multiple inheritance of implementation entirely, reserving it for multiple inheritance of pure interfaces โ abstract classes containing only pure virtual functions (cpp1-7's own = 0 syntax) โ which sidesteps the diamond problem entirely, since there's no actual data to duplicate.
Contrasted With Rust's Trait-Based Composition
Rust deliberately has no multiple inheritance of structs at all. A struct can implement any number of traits, and traits can have default method implementations โ but a struct never inherits data from multiple sources the way a C++ class can. This sidesteps the diamond problem structurally, by design, rather than needing a virtual-inheritance fix bolted on afterward. Rust's own answer to "I need behavior from multiple sources" is composition and multiple trait implementation, not multiple inheritance of state.
| Concept | C++ Multiple Inheritance | Rust Traits |
|---|---|---|
| Inheriting data from multiple sources | possible โ the diamond problem's real cause | not possible at all โ structurally avoided |
| Inheriting behavior from multiple sources | possible โ via multiple abstract interfaces | possible โ implement any number of traits |
| Fix for ambiguous shared bases | virtual inheritance โ a real, non-trivial mechanism | n/a โ the ambiguity was never possible to create |
virtual, but not the other, produces inconsistent, confusing behavior. Both paths need to agree on using virtual inheritance for the fix to actually work.
Coding Challenges
Reproduce this chapter's Animal/Swimmer/Flyer/Duck diamond WITHOUT virtual inheritance, and report the exact compile error produced when trying to access d.name.
๐ View solutionFix Challenge 1 by adding virtual to both Swimmer's and Flyer's inheritance from Animal, then successfully set and print d.name.
๐ View solutionExplain precisely why the diamond problem cannot occur in Rust at all, tying your answer to the specific structural difference between how a C++ class inherits from a base and how a Rust struct implements a trait.
๐ View solutionChapter 1 Quick Reference
class D : public B1, public B2 {}โ multiple inheritance, unavailable in C or Rust in this form- Diamond problem โ a shared base reached through two paths produces two ambiguous data copies
virtual public Baseon both paths โ the fix, ensuring exactly one shared sub-object- Prefer multiple inheritance of pure abstract interfaces over multiple inheritance of implementation
- Rust avoids this entirely โ traits provide behavior, never inherited data, so the ambiguity can't arise
- Next chapter: templates, deeper โ specialization, variadic templates, and a light touch on C++20 concepts
Templates, Deeper
cpp2-1 deferred two things: the deeply confusing pre-C++20 template error, and full template depth. This chapter delivers both.
Template Specialization
A different implementation of a template for one specific type.
A real, well-known example: std::vector<bool> has its own famous specialization, bit-packing elements instead of storing full bytes โ a genuinely different implementation from every other std::vector<T>.
Partial Specialization
Specializing for a category of types rather than one exact type โ a real, useful middle ground.
Variadic Templates
template<typename... Args> accepts an arbitrary number of template arguments.
This is genuinely what powers std::make_unique โ used unexplained since cpp2-4 โ and similar "forward any number of constructor arguments" functions throughout the STL.
The Confusing Error, Resolved โ C++20 Concepts
cpp2-1's own deferred promise: concepts let a template constrain what operations T must support, directly in the template's own declaration.
This moves the error from deep inside the template's own instantiated body to a clear, readable message at the call site itself, naming exactly which requirement wasn't met โ a genuine, real quality-of-life improvement.
Contrasted With Rust's Own Trait Bounds
Rust's fn sum<T: Add>(a: T, b: T) -> T has always worked this way, since Rust's very first stable release. Trait bounds constraining a generic parameter aren't a recent addition to Rust the way concepts are to C++ (added in C++20, decades after templates themselves) โ another point where Rust simply had, by default and from day one, a discipline C++ has only recently, and optionally, caught up to.
| Concept | C++ Pre-C++20 | C++20 Concepts | Rust Trait Bounds |
|---|---|---|---|
| Constraint expressed where? | nowhere โ implicit, discovered on instantiation | in the template declaration itself | in the function signature itself, since day one |
| Error on unmet requirement | deep, confusing, nested inside the instantiation | clear, at the call site, naming the requirement | clear, at the call site, always |
| When introduced | templates since ~1998 | concepts added in C++20 | present from Rust's first stable release |
Coding Challenges
Write a class template Box
Write a variadic template function sum_all(Args... args) that adds together an arbitrary number of arguments using a fold expression, and call it with 2, 3, and 5 arguments, printing each result.
๐ View solutionExplain precisely what C++20 concepts change about WHERE a template's requirements are checked and reported, and why Rust never needed an equivalent feature added later in its own history.
๐ View solutionChapter 2 Quick Reference
- Full specialization โ a completely different implementation for one specific type
- Partial specialization โ a different implementation for a category of types (e.g. all pointers)
- Variadic templates โ
typename... Args, an arbitrary number of arguments; powersmake_uniqueand similar STL functions - C++20 concepts โ constraints declared up front, clear errors at the call site instead of deep inside instantiation
- Rust's trait bounds have worked this way since its first stable release โ no equivalent retrofit was ever needed
- Next chapter: the Rule of Five and copy/move semantics, in full mechanical depth
The Rule of Five & Copy/Move Semantics In Depth
cpp1-5 and cpp2-8 both previewed the Rule of Five. This chapter is the full mechanical depth โ exactly which declaration suppresses which auto-generation.
The Five Special Member Functions, Named Precisely
- Destructor โ
~T() - Copy constructor โ
T(const T&) - Copy assignment โ
T& operator=(const T&) - Move constructor โ
T(T&&) - Move assignment โ
T& operator=(T&&)
What the Compiler Generates by Default
If none are declared, the compiler generates all five automatically โ each doing the "obvious" member-wise thing. This default is genuinely correct for classes made entirely of other well-behaved types. This is the Rule of Zero (cpp2-8), explained mechanically: it works precisely because the compiler-generated defaults are already correct when every member is itself well-behaved.
The Suppression Rules โ When Declaring One Function Silently Deletes Others
The real mechanical depth. Declaring a destructor suppresses automatic generation of the move constructor and move assignment entirely โ not a bad default, simply not generated at all. Declaring any of copy constructor, copy assignment, move constructor, or move assignment suppresses both move operations from being auto-generated.
A Real Table โ What Suppresses What
| You declare | Move ctor/assignment | Copy ctor/assignment |
|---|---|---|
| Nothing | auto-generated | auto-generated |
| Destructor | suppressed | still generated (deprecated, legacy-only) |
| Copy ctor or copy assignment | suppressed | the other is still auto-generated |
| Move ctor or move assignment | the other is still auto-generated | suppressed entirely |
= default and = delete
Modern C++11 syntax to be explicit about intent, rather than relying on implicit suppression rules.
cpp2-4's own unique_ptr uses exactly = delete internally on its own copy constructor โ not magic, simply this syntax.
Rust's Approach Contrasted
Rust has no equivalent implicit-generation-with-suppression-rules system at all. Copy/Clone must be explicitly derived or implemented โ #[derive(Clone, Copy)] โ and move is simply the default for every type unless it opts into Copy. No five-function dance, no suppression table to memorize. A genuinely simpler, more explicit design, deliberately trading C++'s flexibility for predictability.
| Concept | C++ | Rust |
|---|---|---|
| Default behavior | complex, order-dependent generation/suppression rules | move by default; Copy/Clone must be explicitly opted into |
| Explicit intent | = default / = delete, optional | #[derive(...)], required to opt in |
| Rules to memorize | a real, non-trivial suppression table | none โ one consistent default |
Coding Challenges
Write a class with a hand-written destructor and nothing else declared. Attempt to move-construct an instance of it, and report whether the move constructor or the copy constructor actually runs (add print statements to both to observe).
๐ View solutionWrite a class that explicitly deletes its copy constructor with = delete, then attempt to copy an instance of it and report the resulting compile error.
๐ View solutionExplain precisely why a class declaring only a destructor still ends up copying (not moving) when passed by value, tying your answer directly to this chapter's own suppression table.
๐ View solutionChapter 3 Quick Reference
- The five: destructor, copy ctor, copy assignment, move ctor, move assignment
- Declare nothing โ all five are auto-generated correctly (the Rule of Zero, mechanically explained)
- Declare a destructor โ move operations are suppressed entirely; copy is still generated (deprecated)
- Declare any copy or move operation โ both move operations are suppressed
= default/= deleteโ state intent explicitly, rather than relying on implicit rules- Rust has no equivalent system โ move by default, Copy/Clone opted into explicitly, no suppression table
- Next chapter: concurrency in modern C++ โ std::thread, std::mutex, std::atomic
Concurrency in Modern C++
c3-3 built a race condition with raw pthreads. This chapter shows modern C++'s own equivalents โ genuinely nicer to use, but not a different safety story.
std::thread
A higher-level replacement for pthread_create/pthread_join โ but with a real, important gotcha: a std::thread still joinable when its destructor runs calls std::terminate. It does not automatically join or detach the way an RAII-styled course might suggest; .join() or .detach() must be called explicitly before destruction.
A Worked Example โ Revisiting c3-3's Race Condition
The exact same race โ the higher-level wrapper changes nothing about the underlying problem, only the syntax used to create the threads.
std::mutex and std::lock_guard
std::mutex is the same concept as c3-3's own pthread_mutex_t. std::lock_guard<std::mutex> is a genuine RAII wrapper around locking โ the direct fix to c3-3's own "forgotten unlock deadlocks" warning. Its destructor unlocks automatically, on any exit path, including an exception โ exactly cpp2-6's own exception-safety mechanism, applied specifically to locking.
std::atomic
A genuinely different, lower-overhead alternative for simple shared counters and flags specifically.
Resolves c3-3's own race condition with no lock/unlock pairing whatsoever โ a real, meaningful alternative worth knowing for exactly this narrow case.
Contrasted With Rust's Send and Sync, Revisited
std::mutex/std::lock_guard are a genuine ergonomic improvement over c3-3's raw pthread_mutex_t โ automatic unlocking via RAII, no more forgotten-unlock deadlocks. But the same fundamental gap from c3-3 remains: nothing in C++'s type system ties a std::mutex to the specific data it protects, and nothing prevents accessing that data without holding the lock at all. Rust's Mutex<T> โ wrapping the data itself, checked by the Send/Sync marker traits at compile time โ is still the genuinely stronger guarantee. lock_guard is a real ergonomic win, not a safety win at the type-system level.
| Concept | pthreads (c3-3) | Modern C++ | Rust |
|---|---|---|---|
| Thread creation | pthread_create/join | std::thread, .join()/.detach() | std::thread::spawn |
| Locking | manual lock/unlock, forgettable | std::lock_guard โ RAII, automatic unlock | Mutex<T> wraps the data itself |
| Mutex-to-data relationship | none โ separate variables | still none โ separate variables | enforced by the type system |
| Accessing data without locking | compiles, UB at runtime | compiles, UB at runtime | not possible โ no name refers to the data directly |
std::atomic for simple counters/flags; std::lock_guard (or std::unique_lock for more flexibility) for anything needing genuine mutex protection. Avoid raw lock()/unlock() pairs in new code entirely.
lock_guard, std::thread does not solve this via RAII โ a thread still joinable when destroyed calls std::terminate immediately. Genuinely easy to forget on an early return or exception path, echoing cpp2-6's own exception-safety themes, but not automatically handled here the way locking is.
Coding Challenges
Rewrite this chapter's own counter race using std::thread instead of pthreads, run it a few times, and report whether the final value is consistently 200,000.
๐ View solutionFix Challenge 1 two different ways: once using std::lock_guard around the increment, and once using std::atomic
Explain precisely why std::lock_guard is described as a real ergonomic improvement over c3-3's raw pthread_mutex_t but NOT a safety improvement at the type-system level, tying your answer to what Rust's Mutex<T> does differently.
๐ View solutionChapter 4 Quick Reference
std::threadโ must be explicitly.join()ed or.detach()ed, or its destructor callsstd::terminatestd::lock_guard<std::mutex>โ RAII locking, unlocks automatically on any exit path including exceptionsstd::atomic<T>โ lock-free, for simple counters/flags specifically- Modern C++'s tools are a real ergonomic improvement over raw pthreads โ not a type-system safety improvement
- The mutex-to-data gap from
c3-3remains โ Rust'sMutex<T>still closes it, C++'s doesn't - Next chapter: undefined behavior in C++ โ building on
c3-4's own C-level UB catalog
Undefined Behavior in C++
This chapter doesn't re-derive c3-4's own UB deep dive โ everything there still applies, unchanged. This is only what's genuinely new to C++ specifically.
Everything From c3-4 Still Applies
Signed overflow, out-of-bounds access, dangling pointers, use-after-free, double-free, strict aliasing, excessive shifts, data races โ every category c3-4 catalogued applies unchanged, since C++ inherits C's entire UB model at the language-core level.
Object Slicing
A genuinely new-to-C++ trap: assigning a derived-class object to a base-class object by value โ not through a pointer or reference โ "slices off" the derived part entirely, leaving only the base portion. Not technically undefined behavior in the strict sense (copying just the base part is well-defined), but almost always a serious logic bug, since cpp1-7's own virtual dispatch is lost entirely.
This is exactly what cpp2-6's own "catch by reference to avoid slicing" tip was pointing toward โ now fully explained.
Pure Virtual Function Called From a Constructor
A genuinely real, subtle trap: calling a virtual function from a base class's own constructor does not dispatch to a derived override, even if the object being constructed is ultimately a derived type. Calling a pure virtual function this way is genuinely undefined behavior, not merely surprising.
Why Construction Order Causes This
During a base class's own constructor, the object's hidden vtable pointer is set to point at the base class's vtable โ not the derived one. It's updated to point at successively more-derived vtables as each level of construction completes, finishing only once the most-derived constructor runs. Calling a virtual function mid-construction sees whatever vtable is currently set, never the final one.
Uninitialized Member Variables
C++ class members of built-in types (int, pointers, etc.) are not automatically zero-initialized unless explicitly done so in the constructor โ a real, common gotcha, especially coming from a language with different defaults. Rust simply doesn't allow reading an uninitialized variable at all โ a compile error, closing this exact class of bug structurally.
| Concept | C (c3-4) | C++-specific addition | Rust |
|---|---|---|---|
| Core UB categories | signed overflow, OOB, dangling pointers, etc. | inherited unchanged | structurally prevented in safe code |
| Slicing a polymorphic object | n/a โ no inheritance | a real logic bug, virtual dispatch silently lost | n/a โ no equivalent value-slicing concept |
| Uninitialized member read | UB โ garbage value | UB โ garbage value, same as C | compile error โ cannot read before initializing |
cpp2-6's own advice, now fully justified: catch exceptions by reference, and generally pass/return any polymorphic object by pointer or reference, never by value โ value semantics and virtual dispatch simply don't mix safely.
Coding Challenges
Reproduce this chapter's own Shape/Circle slicing example: assign a Circle to a plain Shape variable by value, call area() on the sliced Shape, and confirm it calls Shape's own implementation rather than Circle's.
๐ View solutionFix Challenge 1 so that calling area() correctly dispatches to Circle's own implementation, without changing anything about how the Circle object itself is constructed.
๐ View solutionExplain precisely why calling a virtual function from a base class's own constructor doesn't dispatch to a derived override, tying your answer to exactly when the object's vtable pointer is updated during construction.
๐ View solutionChapter 5 Quick Reference
- Every UB category from
c3-4applies unchanged in C++ โ nothing re-derived here - Object slicing โ assigning a derived object to a base BY VALUE loses the derived part and virtual dispatch, silently
- Calling a virtual (especially pure virtual) function from a base constructor doesn't see the derived override โ the vtable pointer isn't finalized yet
- C++ built-in-type members are not zero-initialized by default โ unlike Rust, which refuses to compile a read before initialization
- Never pass/return polymorphic objects by value โ always by pointer or reference
- Next chapter: the capstone โ a modern C++ project combining everything from this entire track
Capstone: Building a Small Project
Twenty-two chapters, one piece at a time. This closing chapter builds a small shape inventory โ polymorphic shapes, RAII, templates, smart pointers, exceptions, and STL algorithms combined into one working, modern C++ program.
The Project โ A Shape Inventory
The Shape Hierarchy
cpp1-4/cpp1-5/cpp1-7: a Shape base class with virtual area() and name(), derived classes validating their own input in the constructor and throwing on invalid data (cpp2-6).
Exception Safety in the Constructors
Circle(-5.0) throws before construction ever completes โ a real, direct application of cpp2-6's own constructor-exception material. No RAII resource is ever left half-acquired.
A Generic Repository<T> Template
cpp2-1's own templates, applied for real โ a small, genuinely reusable class template, not shape-specific at all.
Ownership via unique_ptr โ No Slicing, Ever
Shapes are never stored or passed by value โ only via unique_ptr<Shape>, deliberately avoiding cpp3-5's own object-slicing trap by construction. add() takes ownership via std::move, exactly cpp2-4's and cpp2-5's own material applied together.
Sorting With a Lambda
cpp2-3's std::sort plus cpp2-7's own lambda syntax, comparing shapes by area through their owning unique_ptrs.
const-Correctness Throughout
cpp2-8's own discipline, applied everywhere: area()/name() are const; Repository's own size()/operator[] are const-correct too โ nothing in this project mutates state it doesn't need to.
Where Each Piece Came From
| Piece | Chapter |
|---|---|
| Shape hierarchy, RAII, polymorphism | cpp1-4, cpp1-5, cpp1-7 |
| Exception-validated constructors | cpp2-6 |
| Repository<T> template | cpp2-1 |
| unique_ptr ownership, no slicing | cpp2-4, cpp3-5 |
| std::move into the repository | cpp2-5 |
| std::sort with a lambda | cpp2-3, cpp2-7 |
| const-correctness throughout | cpp2-8 |
What's Still Out of Scope, Honestly
No multiple inheritance was needed โ cpp3-1's own material simply wasn't necessary here, a single hierarchy sufficed, itself a realistic outcome. No concurrency (cpp3-4) โ this is a single-threaded tool. No hand-written Rule of Five anywhere โ the entire project follows the Rule of Zero throughout, a deliberate demonstration that modern C++ rarely needs it. C++20 concepts (cpp3-2) weren't used either, kept to plain templates for broader compiler compatibility.
c3-6) established.
new/delete, no slicing, exception-safe construction. But nothing here is enforced by the compiler the way Rust's ownership and borrow rules would be. The discipline is real; it's still discipline, not a guarantee.
Closing the Course & Track
From cpp1-1's compile model to a real, polymorphic, exception-safe, template-based, RAII-everywhere program โ every chapter added one piece of real capability C never had, while this course's own repeated Rust comparisons kept one honest question in view: what does the language enforce, versus what does it merely make possible? The genuine convergence points along the way โ RAII/Drop, monomorphization, unique_ptr/Box, shared_ptr/Rc, lambdas/closures โ show two languages solving similar problems from different starting philosophies. That comparison, sustained across 44 chapters spanning both tracks, is the actual lesson.
Coding Challenges
Identify which specific chapter each of the following pieces of the shape inventory came from: (a) the Repository
Explain why storing shapes as std::vector
Across the entire C++ track, name the single idea that recurs most often, and explain in your own words why understanding it deeply matters more than memorizing any individual keyword or syntax rule.
๐ View solutionChapter 6 Quick Reference โ Course & Track Complete
- The shape inventory combines: polymorphism (cpp1-7), RAII (cpp1-5), templates (cpp2-1), smart pointers (cpp2-4), move semantics (cpp2-5), exceptions (cpp2-6), lambdas (cpp2-7), const-correctness (cpp2-8), and avoids slicing (cpp3-5)
- Still out of scope, honestly: multiple inheritance, concurrency, hand-written Rule of Five, C++20 concepts โ none were needed here
- Real capability, not compiler-enforced guarantees โ the discipline is genuine, but still discipline, not Rust's kind of guarantee
- Course 3 complete โ C++ Advanced, 6 chapters
- Full C++ track complete โ Fundamentals + Intermediate + Advanced, 23 chapters across 3 courses