C++ Intermediate
A Complete 8-Chapter Programming Course
Table of Contents
- Templates
- The STL โ Containers
- The STL โ Iterators & Algorithms
- Smart Pointers
- Move Semantics & Rvalue References
- Exception Handling
- Lambda Expressions
- const-Correctness & Modern C++ Style
Templates
Course 1 built the OOP foundation. Course 2 starts with the everyday tool that foundation โ and the rest of the standard library โ is actually built on top of.
The Problem Templates Solve
Writing the same logic separately for every type โ a max_int and a separate max_double โ is genuinely repetitive. C has no generic mechanism beyond void * plus manual casting (losing type safety entirely) or macros (c2-4's own text-substitution trap, applied to "generic" code with none of the type checking a real function would provide).
Function Templates
Compile-Time Monomorphization
The real mechanism: the compiler generates a separate, fully concrete function for each distinct type actually used. Calling max_value with int and again with double produces two genuinely distinct compiled functions, each fully type-checked and optimized exactly as if hand-written for that specific type โ not a single generic, runtime-dispatched function. Zero runtime overhead, at the cost of larger compiled binary size. This is a direct parallel to rust2-3's own generics chapter โ Rust uses the identical monomorphization strategy, another rare point of real convergence between the two languages, alongside cpp1-5's own RAII/Drop parallel.
Class Templates
This is exactly the shape cpp2-2's own STL containers use โ std::vector<int> genuinely is a template instantiation, not a special built-in construct.
Template Type Deduction
In most cases the compiler infers T from the arguments automatically โ max_value(3, 7) works with no need to write max_value<int>(3, 7) explicitly, though the explicit form is always allowed.
A Compile Error You'll See โ No Matching Function
If T doesn't support an operation the template body actually uses (e.g. calling max_value on a type with no > operator defined), the error surfaces at the instantiation site, often with a genuinely confusing, deeply nested message โ a well-known rough edge of pre-C++20 templates. C++20 concepts improve this substantially; full depth is deferred to cpp3-2's own "Templates, Deeper" chapter.
| Concept | C | C++ Templates | Rust Generics |
|---|---|---|---|
| Generic code | void* + manual casting, or macros โ no type safety | templates โ fully type-checked | generics โ fully type-checked |
| Compilation strategy | n/a | monomorphization โ one concrete copy per type used | monomorphization โ the same strategy |
| Runtime overhead | n/a | none | none |
cpp1-3/c2-5's own header/definition-separation convention: the compiler needs to see a template's full definition at every point it's instantiated, so template code is typically written entirely in the header itself, rather than declared in a header and defined in a matching .cpp file.
Coding Challenges
Write a function template min_value(T a, T b) returning the smaller of two values, and call it with two ints and two doubles, printing both results.
๐ View solutionWrite a class template Pair
Explain precisely what monomorphization means, and why calling max_value with both int and double arguments in the same program results in genuinely two separate compiled functions rather than one generic function handling both cases at runtime.
๐ View solutionChapter 1 Quick Reference
template<typename T>โ declares a function or class template- Monomorphization โ one fully concrete, type-checked copy generated per distinct type actually used, zero runtime overhead
- A direct parallel to Rust's own generics โ the identical compilation strategy
std::vector<int>is literally a class template instantiation, not a special built-in- Template errors surface at the instantiation site โ often confusing pre-C++20; concepts (
cpp3-2) improve this - Template definitions usually live entirely in headers, not split across a .cpp file
- Next chapter: the STL's containers โ finally resolving C's own "no standard containers" gap
The STL โ Containers
c3-2 built a linked list and a hash table entirely by hand, because C offers nothing. This chapter is the direct payoff: the same shapes, already written, tested, and optimized, as class templates.
std::vector
A growable array. c3-2's own linked list required a manual malloc/free pair per node; std::vector manages its own memory internally via RAII โ the caller never calls free at all.
std::vector Under the Hood
Contiguous memory, not a linked list โ growing occasionally reallocates a larger block and moves every element over. Despite this, push_back is amortized O(1) โ a real, important performance characteristic: individual calls are occasionally more expensive (during a reallocation), but averaged over many calls, the cost per call stays constant.
std::map
A direct resolution of c3-2's own hand-built hash table. std::map specifically is typically a balanced tree internally, not a hash table โ giving O(log n) operations and genuine key ordering as a side benefit. std::unordered_map is the hash-table-based sibling โ closer to c3-2's own separate-chaining structure โ giving O(1) average operations but no ordering.
std::string
A real, growable string type โ contrasted directly with c1-8's own char arrays: no manual null-termination tracking, no manual sizing, and .length()/.size() are O(1), unlike c1-8's own O(n) strlen scan. Concatenation uses +, via exactly cpp1-6's own operator overloading mechanism.
RAII All the Way Down
Every one of these containers is itself an RAII wrapper โ exactly cpp1-5's own mechanism. A vector's destructor frees every element's memory automatically when the vector goes out of scope. Nothing here is new magic โ it's cpp1-5's own idea, already written for you by the standard library.
| Concept | C (c3-2) | C++ STL |
|---|---|---|
| Growable list | a hand-built linked list, manual free per node | std::vector โ RAII-managed, contiguous memory |
| Key-value store | a hand-built hash table, separate chaining | std::map (ordered) / std::unordered_map (hash-based) |
| String length | strlen โ O(n), scans for '\0' | .length() โ O(1), stored directly |
std::vector unless there's a specific reason for something else โ std::map for real key lookups, std::unordered_map when ordering genuinely doesn't matter and average-case speed does. This matches the real-world idiomatic default in C++.
std::vector to reallocate moves every element to a new block of memory โ any iterator, pointer, or reference into the old memory is left dangling. Continuing to use one afterward is undefined behavior, a genuinely real and easy-to-hit trap. Iterators get full treatment in cpp2-3.
Coding Challenges
Create a std::vector
Create a std::map
Explain why std::vector's push_back is described as "amortized O(1)" rather than simply "O(1)," and what specifically happens during the occasional more expensive call that the amortized average accounts for.
๐ View solutionChapter 2 Quick Reference
std::vector<T>โ a growable, RAII-managed, contiguous array; resolvesc3-2's linked-list gapstd::map<K, V>โ ordered, tree-based;std::unordered_mapโ hash-based, closer toc3-2's own structurestd::stringโ O(1) length, no manual null-termination,+for concatenation via operator overloading- Every STL container is itself an RAII wrapper โ
cpp1-5's own idea, pre-written - Reallocation-triggering operations invalidate existing iterators/pointers/references โ a real, common trap
- Next chapter: iterators and algorithms โ std::sort, std::find, and generic operations over any container
The STL โ Iterators & Algorithms
cpp2-2's containers gave C++ real, standard data structures. This chapter is what makes them interchangeable: one std::sort, working identically across every container that offers iterators.
What an Iterator Is
A generalized pointer-like object. begin() points at the first element; end() points one past the last โ never the last element itself, the "half-open range" convention [begin, end). Dereferencing (*it) gives the element; ++it advances.
Iterating With Iterators
cpp1-2's own range-based for (auto x : container) is genuinely sugar for exactly this iterator pattern, generated automatically underneath.
Why Iterators Exist โ Generic Algorithms Over Any Container
The real payoff: std::sort/std::find/and friends don't know or care whether they're operating on a vector, a list, or a raw array โ they only need begin()/end() iterators satisfying certain requirements. This is cpp2-1's templates applied one level up, at the algorithm level rather than the container level โ std::sort is itself a function template.
std::sort
Sorts a range in place. c3-2's own hand-built structures never included a general sorting algorithm at all โ writing one in C means implementing the actual algorithm by hand, every time.
std::find
A linear search returning an iterator to the found element, or end() if not found โ the end()-as-sentinel pattern is idiomatic and used constantly across the STL.
Iterator Invalidation, Revisited
cpp2-2's own warn-box flagged reallocation invalidating iterators โ with iterators now properly introduced, the fuller picture: erasing or inserting into a vector can invalidate iterators at or after the modification point too, not just growth-triggered reallocation.
| Concept | C (c3-2) | C++ STL |
|---|---|---|
| Traversal | hand-written loop, container-specific | iterators โ the same syntax across any container |
| Sorting | implement the algorithm by hand | std::sort โ one call, any container |
| Searching | hand-written traversal loop | std::find โ returns an iterator, end() as "not found" |
std::sort/std::find and the rest of the STL's algorithm library are genuinely less error-prone than a hand-written loop, and usually at least as fast โ real implementations are heavily optimized.
begin()/end(), or one that's already been invalidated, compiles cleanly and is undefined behavior at runtime โ the type system doesn't tie an iterator to "the container it's still safely valid for," the way Rust's borrow checker would.
Coding Challenges
Create a std::vector
Use std::find to search a std::vector
Explain why the range-based for loop (for (auto x : container)) is described as "sugar" over the explicit iterator loop, and rewrite one range-based for loop yourself as its equivalent explicit iterator-based form.
๐ View solutionChapter 3 Quick Reference
begin()/end()โ the half-open range[begin, end);end()points one past the last element- Range-based
foris sugar over the exact same iterator pattern, generated automatically - Algorithms like
std::sort/std::findwork identically across any container offering iterators โ templates, one level up std::findreturnsend()as its "not found" sentinel โ checkit != container.end()- Erasing/inserting/reallocating can invalidate existing iterators โ using one afterward is undefined behavior, uncaught by the compiler
- Next chapter: smart pointers โ unique_ptr and shared_ptr, a modern RAII alternative to raw malloc/free
Smart Pointers
cpp1-5's own Challenge 2 built a leak on purpose: a Circle* allocated with new, never deleted, because RAII only protects objects whose own lifetime is tied to a scope โ and a raw pointer variable's scope has nothing to do with the heap object it points at. This chapter is the real fix.
The Gap Raw Pointers Leave Open
A raw pointer going out of scope destroys only the pointer itself, never the object it points to. Smart pointers close this gap by wrapping the raw pointer inside an RAII-managed object โ exactly cpp1-5's own idea, applied specifically to ownership.
unique_ptr โ Sole Ownership
Exactly one unique_ptr owns a given object at a time. When it's destroyed, it automatically deletes the object it owns โ genuinely just cpp1-5's own IntArray wrapper, generalized and already written for you.
unique_ptr Cannot Be Copied
A real, deliberate constraint: copying a unique_ptr would mean two owners both believing they're responsible for deleting the same object โ exactly the double-free class from c2-3's own catalog. The compiler enforces this directly; attempting to copy one is a compile error, not a runtime risk.
Ownership can be transferred via std::move โ a direct preview of cpp2-5's own Move Semantics chapter. After the move, the source unique_ptr becomes empty.
shared_ptr โ Shared Ownership via Reference Counting
Multiple shared_ptrs can jointly own the same object, tracked by a hidden reference count. The object is deleted only when the last owning shared_ptr is destroyed.
The Real Comparison to Rust
unique_ptr is essentially Rust's Box<T> โ sole ownership, moved not copied, deterministic destruction. shared_ptr is essentially Rust's Rc<T> โ reference-counted shared ownership, the identical strategy. This is genuinely another point of real convergence, alongside cpp1-5's RAII/Drop parallel and cpp2-1's monomorphization parallel โ C++'s and Rust's own ownership models visibly converge here.
A Genuine Gap Even Here โ No Compile-Time Enforcement
The honest caveat: nothing stops a programmer from also keeping a raw pointer to the object a unique_ptr owns, and using that raw pointer after the unique_ptr is destroyed โ a genuine use-after-free, still fully possible. Smart pointers eliminate the forgetful class of memory bug โ nobody forgets to call delete anymore โ not the deliberate-misuse class. Rust's borrow checker closes both.
| Concept | Raw pointer | unique_ptr | shared_ptr | Rust |
|---|---|---|---|---|
| Ownership | none โ just an address | sole owner | shared, ref-counted | Box<T> / Rc<T> |
| Copy behavior | freely copyable | compile error โ must move | copyable โ increments the count | move / Rc::clone |
| Compile-time misuse prevention | none | prevents double-ownership only | prevents double-ownership only | prevents use-after-free entirely |
unique_ptr first โ reserve shared_ptr for cases where genuine shared ownership is actually needed, since reference counting carries real runtime overhead unique_ptr doesn't have at all.
shared_ptr to the other never reach a zero reference count between them โ a genuine memory leak, smart pointers notwithstanding. std::weak_ptr exists specifically to break such cycles, a non-owning reference that doesn't contribute to the count.
Coding Challenges
Rewrite cpp1-5's own Challenge 2 (a Circle allocated with new, never deleted) using std::unique_ptr instead, adding a destructor message to Circle to confirm it's now genuinely destroyed automatically.
๐ View solutionCreate two shared_ptrs jointly owning the same object, print the reference count after each is created (use_count()), then let one go out of scope and print the count again to observe it decrease.
๐ View solutionExplain precisely why smart pointers are described as eliminating the "forgetful" class of memory bug but not the "deliberate misuse" class, giving a concrete scenario where a raw pointer kept alongside a unique_ptr still produces a genuine use-after-free.
๐ View solutionChapter 4 Quick Reference
std::unique_ptr<T>โ sole ownership, cannot be copied, transferred only viastd::movestd::shared_ptr<T>โ reference-counted shared ownership, deleted when the last owner is destroyed- unique_ptr โ Rust's Box<T>; shared_ptr โ Rust's Rc<T> โ a real convergence in ownership model
- Smart pointers eliminate forgotten-
deletebugs, not deliberate raw-pointer misuse โ Rust's borrow checker closes both std::weak_ptrโ a non-owning reference, the escape hatch forshared_ptrreference cycles- Next chapter: move semantics and rvalue references โ the mechanism
std::movealready previewed here
Move Semantics & Rvalue References
cpp2-4 used std::move to transfer a unique_ptr's ownership without explaining why it worked. This chapter is that explanation, in full.
The Problem โ Unnecessary Copies
Returning a large object like a vector by value historically meant a full, expensive deep copy. Before C++11, this was a genuine performance problem, pushing programmers toward awkward workarounds โ passing output parameters by reference instead of simply returning by value.
lvalues and rvalues
A necessary vocabulary: an lvalue has a name and persistent identity โ an ordinary variable. An rvalue is a temporary with no persistent identity โ a literal, or a function's return value. T& binds only to lvalues; T&& (a new syntax โ a rvalue reference) binds only to rvalues. This distinction is exactly what lets the compiler choose between a copy and a move, at compile time, based on which kind of value is actually being used.
Move Constructors and Move Assignment
A class can define a move constructor, taking an rvalue reference. Instead of copying the source object's resources, it steals them โ pointer/handle fields are copied over, then explicitly nulled out in the source, so the source's own destructor doesn't also try to free the same resource. This directly avoids c2-3's own double-free bug class, by deliberate design.
std::move โ Casting an lvalue to an rvalue
The real mechanism: std::move doesn't actually move anything by itself โ it's purely a cast, converting an lvalue (which would normally bind to T&, triggering a copy) into an rvalue reference (T&&), so the move constructor gets selected instead of the copy constructor. A genuinely important, often-misunderstood fact: std::move performs no action of its own at all.
After a Move, the Source Is in a "Valid But Unspecified" State
A precise, real rule: a moved-from object is guaranteed to still be safely destructible and (usually) safely reassignable โ but its actual value is unspecified. Reading it for anything else is a logic bug, though not undefined behavior the way c1-7's dangling pointer would be โ a genuinely important, precise distinction.
Contrasted With Rust's Own Move-By-Default Semantics
Rust performs this exact optimization automatically and by default, per rust1-3's own ownership model โ passing or returning a value moves it unless the type is Copy, with no special syntax needed and no possibility of forgetting to move efficiently. C++ requires explicitly opting in via std::move, or relying on the compiler's own automatic Return Value Optimization in specific cases โ another point where Rust simply enforces, by default, a discipline C++ makes available but optional.
| Concept | C++ | Rust |
|---|---|---|
| Moving instead of copying | opt-in โ std::move, or compiler RVO | automatic and default โ per rust1-3's ownership model |
| Forgetting to move efficiently | genuinely possible โ an unnecessary copy happens silently | not possible โ the compiler always moves by default |
| Using a moved-from value | a logic bug (valid but unspecified), not UB | a compile error โ the compiler tracks it |
std::move at all โ the compiler elides the copy/move entirely in many cases. Worth knowing so as not to sprinkle std::move defensively where it isn't needed.
std::move(a) and then continuing to read a's value afterward (rather than only destroying or reassigning it) silently reads whatever unspecified state the object was left in. Not undefined behavior โ but definitely wrong.
Coding Challenges
Write a class with a raw pointer member and a move constructor that steals the pointer and nulls out the source. Move-construct one instance from another, then print a message confirming the source's pointer is now nullptr.
๐ View solutionMove a std::string into another variable with std::move, then print the moved-from string's value. Explain what you observe and why reading it afterward is a logic bug rather than undefined behavior.
๐ View solutionExplain precisely why std::move is described as "performing no action of its own" โ what does it actually do at the type-system level, and what causes the move constructor to actually run afterward?
๐ View solutionChapter 5 Quick Reference
- lvalue โ has a name/identity; rvalue โ a temporary with none
T&&โ an rvalue reference, binds only to rvalues, enables move constructors/assignment- A move constructor steals resources and nulls the source, avoiding a double-free
std::moveis purely a cast to an rvalue reference โ it moves nothing by itself- A moved-from object is valid but unspecified โ safe to destroy/reassign, a logic bug (not UB) to read otherwise
- Rust moves automatically by default; C++ requires opting in via
std::moveor relying on RVO - Next chapter: exception handling โ try/catch/throw, and RAII's role in exception safety
Exception Handling
cpp1-5 previewed exception safety briefly. This chapter delivers it in full โ and closes with the honest, three-way comparison this whole course has been building toward.
C's Total Absence of Error Propagation
Worth naming directly, for the first time: C has no exceptions, no Result type, nothing beyond a manual return code or errno that the caller must remember to check, every single time, with nothing enforcing that check at all.
throw, try, catch
Stack Unwinding
When an exception is thrown, the stack unwinds: every function between the throw site and the matching catch exits, and every local object along the way has its destructor called, in reverse order of construction. This is exactly cpp1-5's own "Exception Safety, Briefly" preview, now shown in full.
RAII's Role in Exception Safety
The real payoff: because destructors run during unwinding, RAII-managed resources โ smart pointers, containers, file handles wrapped in RAII types โ are cleaned up automatically even when an exception is thrown mid-function, with zero additional code from the programmer. A raw resource acquired with new/malloc and manually freed at a function's end leaks if an exception is thrown before that manual free line is ever reached โ a genuine, concrete argument for using RAII wrappers pervasively, not just a style preference.
Standard Exception Types
std::exception is the base type, with std::runtime_error, std::out_of_range, and others derived from it. Catch by reference (const std::exception&) to avoid slicing โ a real forward pointer to cpp3-1's own object-slicing material.
Exceptions vs. Return Codes vs. Rust's Result<T, E>
Three real, genuinely different guarantees. C's manual return codes/errno: nothing enforces checking them โ silently ignorable. C++ exceptions: an uncaught exception terminates the program rather than silently continuing with a wrong value โ genuinely stronger than C โ but which exceptions a function might throw is invisible in its own signature, a real, honest downside. Rust's Result<T, E>: the possibility of failure is part of the function's own type, and the compiler forces the caller to at least acknowledge it โ via ?, .unwrap(), or a match โ genuinely the strongest of the three.
| Concept | C | C++ | Rust |
|---|---|---|---|
| Failure signaling | manual return code / errno | exceptions โ throw/catch | Result<T, E> |
| Ignoring failure silently | fully possible โ nothing enforces a check | uncaught exception terminates the program | compile-time forced acknowledgment |
| Visible in the function's own type | no | no โ throw specifications aren't part of the signature | yes โ Result<T, E> is the return type itself |
std::terminate โ the entire program crashes outright. Destructors should essentially never throw; a real, important C++ idiom.
Coding Challenges
Write a function that throws a std::runtime_error if a passed-in int is negative, and a try/catch block in main that calls it with a negative value, printing the caught exception's message via e.what().
๐ View solutionWrite a class with a destructor that prints a message, create an instance inside a try block, throw an exception after creating it, and catch the exception in main. Observe when the destructor's message actually prints relative to the catch block.
๐ View solutionExplain precisely why Rust's Result<T, E> is described as the strongest of the three failure-signaling guarantees compared in this chapter, tying your answer to what specifically is and isn't visible in a function's own type signature in each language.
๐ View solutionChapter 6 Quick Reference
throw/try/catchโ C++'s error-propagation mechanism, entirely absent from C- Stack unwinding โ every local object's destructor runs, in reverse order, between the throw and the catch
- RAII resources clean up automatically during unwinding โ a raw resource with manual cleanup leaks on an exception
- Catch by
const std::exception&to avoid slicing (cpp3-1) - C: silently ignorable failure. C++: terminates if uncaught, but invisible in the type signature. Rust: forced compile-time acknowledgment via
Result<T, E> - Destructors should never throw โ doing so during unwinding calls
std::terminate - Next chapter: lambda expressions โ closures in C++, contrasted with Rust's own
Lambda Expressions
cpp2-3's std::sort works with any callable โ this chapter introduces the concise way real C++ code writes one inline, and reveals it's built from a mechanism already covered.
Lambda Syntax
[capture](params) { body } โ a lambda passed directly as std::sort's custom comparator, exactly cpp2-3's own algorithm-plus-callable pattern.
Capture Lists
The real new concept: how a lambda accesses variables from its surrounding scope.
[]โ captures nothing[=]โ captures everything used, by value (a copy)[&]โ captures everything used, by reference[x]โ captures justx, by value[&x]โ captures justx, by reference
What a Lambda Actually Is โ A Class With operator()
The real mechanism, connecting directly back to cpp1-6: a lambda is compiler-generated syntactic sugar for an anonymous class, with captured variables as member fields and an overloaded operator() containing the lambda's body. Not a new language feature at its core โ cpp1-6's own mechanism, applied automatically. Capturing by value copies into those member fields at the point the lambda is created, not when it's called โ a real, sometimes-surprising timing detail.
std::function
A type-erased wrapper capable of holding any callable matching a given signature โ a lambda, a function pointer (c2-7's own material), or a class with operator(). Useful because every lambda genuinely has its own unique, compiler-generated type, even when two lambdas look identical in source โ std::function gives them a common type to be stored or passed around as.
Contrasted With Rust's Own Closures
Rust closures work via the same underlying idea โ the compiler generates a hidden struct capturing referenced variables, implementing one of the Fn/FnMut/FnOnce traits, genuinely comparable to operator(). The real difference: Rust's own capture rules are inferred automatically from how the closure body actually uses each variable (by reference, mutable reference, or move), rather than requiring an explicit capture-list syntax the way C++ does. A genuine ergonomic difference โ but the underlying "anonymous callable object" mechanism is, once again, a real point of convergence, alongside RAII/Drop and monomorphization.
| Concept | C++ Lambda | Rust Closure |
|---|---|---|
| Underlying mechanism | an anonymous class with operator() | a hidden struct implementing Fn/FnMut/FnOnce |
| Capture specification | explicit โ [x], [&x], [=], [&] | inferred automatically from usage |
| Each closure's type | unique, unnameable โ needs std::function or auto | unique, unnameable โ needs a generic bound or Box<dyn Fn> |
[x] over [=], and [&x] over [&] โ both a performance consideration (avoiding unnecessary copies) and a safety consideration (fewer captured references means fewer chances of a dangling reference).
[&] or [&x]) that's stored and called after the enclosing function has returned holds a dangling reference โ exactly cpp1-8's own dangling-reference warning, reached through a lambda's capture instead of an explicit reference variable.
Coding Challenges
Use std::sort with a lambda comparator to sort a std::vector
Write a lambda that captures a local int by value and one that captures the same variable by reference. After creating both lambdas, change the original variable's value, then call both lambdas and explain why they produce different results.
๐ View solutionExplain precisely why a lambda is described as "not a new language feature at its core," tying your answer directly back to cpp1-6's own operator overloading material and what a lambda actually compiles down to.
๐ View solutionChapter 7 Quick Reference
[capture](params) { body }โ lambda syntax; used directly withcpp2-3's own algorithms[]/[=]/[&]/[x]/[&x]โ capture nothing, everything by value, everything by reference, or specific variables- A lambda is compiler-generated sugar for a class with
operator()โcpp1-6's own mechanism, automated - By-value captures copy at lambda creation time, not call time
std::functionโ a type-erased wrapper for any callable, since every lambda's real type is unique and unnameable- Reference-capturing lambdas that outlive their captured variables dangle โ the same bug class as
cpp1-8 - Next chapter: const-correctness and modern C++ style โ closing Course 2
const-Correctness & Modern C++ Style
Course 2 built templates, containers, smart pointers, move semantics, exceptions, and lambdas. This closing chapter ties them together into the actual practice of writing idiomatic modern C++.
const-Correctness, In Full
cpp1-2 introduced const briefly; cpp1-8 covered const T& parameters. The remaining piece: a const member function promises not to modify the object, enforced by the compiler.
This is a real, propagating discipline โ a const object can only call const-qualified member functions on itself.
The Rule of Three
Revisiting cpp1-5's own preview directly: if a class defines any one of a destructor, a copy constructor, or a copy assignment operator, it almost certainly needs all three. It's managing a resource manually, and the compiler's own default-generated versions of the other two would perform a shallow copy โ two objects both believing they own the same resource, leading straight to c2-3's own double-free bug class.
The Rule of Five
C++11 extended it: the move constructor and move assignment operator (cpp2-5's own material) join the same rule. Writing any one of the five is a real signal you likely need to think through all five.
The Rule of Zero
The modern, actually-recommended alternative: don't manage raw resources directly at all. Use RAII wrappers โ smart pointers (cpp2-4), STL containers (cpp2-2) โ for every member, and let the compiler's default-generated destructor, copy, and move operations work correctly automatically, since each member's own RAII type already does the right thing on its own. A genuine, real best practice: the Rule of Five exists to be understood; the Rule of Zero exists to be practiced. Most well-written modern C++ classes need none of the five written by hand.
auto and Range-Based for Loops, Revisited
cpp1-2's auto and cpp2-3's range-based for, named together now as part of "modern C++ style" โ alongside the Rule of Zero, smart pointers, and RAII, this is what a well-written modern codebase actually looks like, in sharp contrast to older, pre-C++11 "C with Classes" style still using raw new/delete and hand-written Rule-of-Three classes everywhere.
Closing Course 2
Templates โ STL containers โ iterators/algorithms โ smart pointers โ move semantics โ exceptions โ lambdas โ now const-correctness and the Rule of Zero tie the whole course together into a genuine practice, not just a list of features. Course 3 goes deeper still: multiple inheritance, templates in more depth, the Rule of Five's full mechanics, concurrency, C++-specific undefined behavior, and a real capstone.
| Concept | Old "C with Classes" | Modern C++ |
|---|---|---|
| Resource management | raw new/delete, hand-written Rule of Three/Five | RAII wrappers, Rule of Zero |
| Containers | hand-rolled or raw arrays | std::vector/std::map and the rest of the STL |
| Type declarations | always spelled out explicitly | auto where it aids readability |
c2-3's own bug catalog.
Coding Challenges
Write a class with a const member function that computes and returns a value from the object's own data without modifying it. Then attempt to call a non-const member function on a const instance of that class, and report the compile error.
๐ View solutionWrite a class that follows the Rule of Zero by holding a std::unique_ptr
Write a class managing a raw new'd int* member with a hand-written destructor, but deliberately no copy constructor or copy assignment operator. Copy an instance of it, let both copies go out of scope, and explain precisely what goes wrong.
๐ View solutionChapter 8 Quick Reference โ Course 2 Complete
- const member functions โ a compiler-enforced promise not to modify the object
- Rule of Three โ destructor, copy constructor, copy assignment: write one, likely need all three
- Rule of Five โ the Rule of Three plus move constructor and move assignment (C++11)
- Rule of Zero โ the modern practice: hold RAII members only, let the compiler generate everything correctly
- Modern C++ style: RAII, smart pointers, the STL,
auto, range-basedforโ vs. older raw new/delete "C with Classes" - Course 2 complete. Course 3 covers multiple inheritance, deeper templates, the Rule of Five in full, modern concurrency, C++-specific UB, and a real capstone