Ch20: Advanced OOP & Memory¶
TL;DR: Custom Containers & Resource Management: Implementing the Rule of Three or Five (destructor, copy/move constructors, copy/move assignments) ensures safe resource management for heap-allocated raw pointer arrays. | Perfect forwarding lets a template function pass its arguments to another function while preserving whether each argument was an lvalue or rvalue — achieved with universal references (
T&&in a template) andstd::forward.
⚡ Quick Reference¶
#include <iostream>
#include <memory>
#include <concepts>
#include <type_traits>
template <typename T = int, size_t max = 10>
class BoxContainer {
private:
T* m_items;
public:
BoxContainer() { m_items = new T[max]; }
~BoxContainer() { delete[] m_items; }
BoxContainer(const BoxContainer& source) requires std::copyable<T> {
m_items = new T[max];
}
BoxContainer& operator=(const BoxContainer& source) {
if (this != &source) {
// copy logic
}
return *this;
}
BoxContainer(BoxContainer&& source) noexcept {
m_items = source.m_items;
source.m_items = nullptr;
}
};
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
static_assert(std::is_default_constructible_v<int>, "Default constructor required");
int main() {
try {
BoxContainer<int, 5> box1;
BoxContainer<int, 5> box2 = std::move(box1);
} catch (const std::exception& ex) {
throw;
}
return 0;
}
Perfect Forwarding¶
#include <iostream>
#include <utility>
#include <string>
void process(const std::string& s) { std::cout << "lvalue overload: " << s << '\n'; }
void process(std::string&& s) { std::cout << "rvalue overload: " << s << '\n'; }
// Universal reference: T&& in a deduced template context (NOT the same as an rvalue reference)
template <typename T>
void relay(T&& arg) {
// std::forward<T> - preserves the value category (lvalue/rvalue) of the original argument
process(std::forward<T>(arg));
}
int main() {
std::string name{"Alice"};
relay(name); // name is an lvalue -> calls process(const std::string&)
relay(std::string{"Bob"}); // temporary is an rvalue -> calls process(std::string&&)
relay(std::move(name)); // explicitly cast to rvalue -> calls process(std::string&&)
return 0;
}
🧠 Core Concepts¶
- Custom Containers & Resource Management: Implementing the Rule of Three or Five (destructor, copy/move constructors, copy/move assignments) ensures safe resource management for heap-allocated raw pointer arrays.
- Modern C++ Constraints: Class templates parameterize data types, C++20 concepts enforce compile-time type requirements (
requires), andstatic_assertvalidates traits to prevent cryptic template build errors. - Move Semantics & Structured Errors: Move semantics (
T&&) optimize resource transfers by stealing pointers from temporaries, whiletry/catchexception hierarchies safely handle runtime anomalies and trigger stack unwinding (RAII). - Universal Reference:
T&&only behaves as a "forwarding reference" whenTis a deduced template parameter (orauto&&). It can bind to both lvalues and rvalues, unlike a plain rvalue reference (std::string&&), which only binds to rvalues. - Reference Collapsing: When a template deduces
Tas a reference type itself, rules likeT& &&collapse toT&. This is the mechanism that makes universal references able to represent either an lvalue or rvalue reference depending on what's passed in. std::forward<T>: Conditionally casts its argument back to an rvalue only ifTwas deduced as a non-reference (meaning the original argument was an rvalue) — this is what "forwards" the original value category through a chain of function calls.
⚠️ Pitfalls (Quick Scan)¶
| Mistake | Fix |
|---|---|
| Reassigning/deleting memory in copy assignment without self-assignment check | Implement a guard check at the beginning: if (this == &source) return; |
Hardcoding std::cout inside polymorphic stream_insert methods |
Always write to the stream parameter out instead of standard console streams |
| Using floating-point types as non-type template parameters (NTTP) | Limit NTTPs to integral types, enums, pointer, or reference variables |
| Forgetting to forward-declare template classes and friend operators before definitions | Always declare templates and their operator friends before defining template classes |
| Catching class exceptions by value instead of catching by reference | Always catch class exceptions using references (catch(std::exception& ex)) |
Rethrowing exceptions using variables throw ex; instead of bare throw; |
Use the bare throw; statement to rethrow caught exceptions natively |
Rethrowing exceptions out of noexcept functions or destructors |
Handle all exceptions internally using try-catch blocks in noexcept methods and destructors |
Using objects after casting them with std::move |
Treat moved-from objects as empty and do not access their previous data states |
| Forgetting to invalidate source pointers after performing a move operation | Always set the source object's pointer to nullptr and its size to 0 |
| Passing an rvalue reference parameter by name to other functions | Wrap named rvalue parameters with std::move when passing them forward |
Using std::move instead of std::forward in a template |
std::move always casts to rvalue, breaking lvalue forwarding — use std::forward<T> |
Assuming T&& is always an rvalue reference |
It's only a universal reference in a deduced template/auto context |
| Forwarding the same argument twice | After the first std::forward moves-from an rvalue, the object may be in a moved-from state |
Using std::forward on a non-template, fixed-type parameter |
Has no effect if T isn't deduced — just use the argument directly |
Forgetting template <typename T> when trying to write a forwarding wrapper |
Without a deduced T, T&& becomes a normal rvalue reference, losing lvalue-binding ability |
📖 Full Details¶
Cause → Effect → Fix with timestamp (click to expand)
* **Reassigning/deleting memory in copy assignment without self-assignment check** -> **Self-assignment (e.g., `box = box;`) deletes its own data first, leading to memory corruption or crashes** -> **Implement a guard check at the beginning: `if (this == &source) return;` (34:00)** * **Hardcoding `std::cout` inside polymorphic `stream_insert` methods** -> **Breaks polymorphic streaming redirect rules (e.g., printing to file/string streams fails)** -> **Always write to the stream parameter `out` instead of standard console streams (35:00)** * **Using floating-point types as non-type template parameters (NTTP)** -> **Compile-time error because floating-point values are not supported as NTTPs** -> **Limit NTTPs to integral types, enums, pointer, or reference variables (212:00)** * **Forgetting to forward-declare template classes and friend operators before definitions** -> **Compilation fails on GCC, Clang, and MSVC compilers** -> **Always declare templates and their operator friends before defining template classes (220:00)** * **Catching class exceptions by value instead of catching by reference** -> **Object slicing discards derived exception details, breaking polymorphic exception handling** -> **Always catch class exceptions using references (`catch(std::exception& ex)`) (272:00)** * **Rethrowing exceptions using variables `throw ex;` instead of bare `throw;`** -> **Copies and slices the exception object, losing its original derived type details** -> **Use the bare `throw;` statement to rethrow caught exceptions natively (273:00)** * **Rethrowing exceptions out of `noexcept` functions or destructors** -> **Immediate invocation of `std::terminate()` aborts the program without stack unwinding** -> **Handle all exceptions internally using try-catch blocks in `noexcept` methods and destructors (276:00, 277:00)** * **Using objects after casting them with `std::move`** -> **Accessing empty, moved-from states (null pointers) leads to undefined behavior or crashes** -> **Treat moved-from objects as empty and do not access their previous data states (329:00)** * **Forgetting to invalidate source pointers after performing a move operation** -> **Both objects retain ownership of the same address, causing double-free crashes on destruction** -> **Always set the source object's pointer to `nullptr` and its size to `0` (330:00)** * **Passing an rvalue reference parameter by name to other functions** -> **The parameter behaves as an lvalue inside the function, triggering expensive copies instead of moves** -> **Wrap named rvalue parameters with `std::move` when passing them forward (333:00)** * **Using `std::move` instead of `std::forward` inside a template relay function** -> **Every argument gets unconditionally cast to an rvalue, so passing an lvalue you still need afterward gets moved-from unexpectedly** -> **Use `std::forward📎 Repo Files¶
39.Practice-BoxContainerType/39.2ConstructingDestroying/main.cpp40.ClassTemplates/40.2YourFirstClassTemplate/main.cpp40.ClassTemplates/40.4NonTypeTemplateParameters/main.cpp40.ClassTemplates/40.7TemplateSpecializations/main.cpp40.ClassTemplates/40.8TemplateSpecializationWithSelectMemberFunctions/main.cpp40.ClassTemplates/40.11StreamInsertionOperatorForClassTemplates/main.cpp40.ClassTemplates/40.12ClassTemplatesWithTypeTraitsAndStaticAsserts/main.cpp40.ClassTemplates/40.13ClassTemplatesWithConcepts/main.cpp40.ClassTemplates/40.14BuiltInConcepts/main.cpp38.2TryCatchBlocks/main.cpp38.10RethrownExceptions/main.cpp38.13NoexceptSpecifier/main.cpp41.MoveSemantics/41.2LvaluesAndRvalues/main.cpp41.MoveSemantics/41.3RvalueReferences/main.cpp41.MoveSemantics/41.5MoveConstructorsMoveAssignmentOperators/main.cpp41.MoveSemantics/41.6MovingLvaluesWithStdMove/main.cpp41.MoveSemantics/41.7InvalidatePointersAfterStdMove/main.cpp41.MoveSemantics/41.08MoveOnlyTypes/main.cpp41.MoveSemantics/41.09PassingByRvalueReference/main.cpp41.MoveSemantics/main.cpp42.FunctionLikeEntities/main.cpp