Skip to content

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) and std::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), and static_assert validates traits to prevent cryptic template build errors.
  • Move Semantics & Structured Errors: Move semantics (T&&) optimize resource transfers by stealing pointers from temporaries, while try/catch exception hierarchies safely handle runtime anomalies and trigger stack unwinding (RAII).
  • Universal Reference: T&& only behaves as a "forwarding reference" when T is a deduced template parameter (or auto&&). 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 T as a reference type itself, rules like T& && collapse to T&. 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 if T was 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(arg)`, which only casts to rvalue when the original argument actually was one** * **Assuming any `T&&` syntax is an rvalue reference** -> **Confusion when the "rvalue reference" happily binds to an lvalue argument** -> **Recognize that `T&&` is a universal/forwarding reference only when `T` is deduced (template parameter or `auto`); a concrete type like `std::string&&` is a true rvalue reference** * **Calling `std::forward` on the same parameter more than once in a function body** -> **The first forward may move-construct another object from an rvalue argument, leaving it in a valid-but-unspecified state for the second use** -> **Forward each argument exactly once, or copy first if it's genuinely needed twice** * **Applying `std::forward` inside a non-template function with a fixed parameter type** -> **No meaningful effect occurs since there's no deduced `T` to base the cast on; the code just looks more complex than necessary** -> **Only use `std::forward` inside function templates (or lambdas with `auto&&` parameters) where `T` is actually deduced** * **Writing a wrapper function without making it a template** -> **`T&&` degenerates into a plain rvalue reference, so the wrapper can no longer accept lvalue arguments at all** -> **Always pair forwarding references with a deduced template parameter: `template void wrapper(T&& arg)`**

📎 Repo Files

  • 39.Practice-BoxContainerType/39.2ConstructingDestroying/main.cpp
  • 40.ClassTemplates/40.2YourFirstClassTemplate/main.cpp
  • 40.ClassTemplates/40.4NonTypeTemplateParameters/main.cpp
  • 40.ClassTemplates/40.7TemplateSpecializations/main.cpp
  • 40.ClassTemplates/40.8TemplateSpecializationWithSelectMemberFunctions/main.cpp
  • 40.ClassTemplates/40.11StreamInsertionOperatorForClassTemplates/main.cpp
  • 40.ClassTemplates/40.12ClassTemplatesWithTypeTraitsAndStaticAsserts/main.cpp
  • 40.ClassTemplates/40.13ClassTemplatesWithConcepts/main.cpp
  • 40.ClassTemplates/40.14BuiltInConcepts/main.cpp
  • 38.2TryCatchBlocks/main.cpp
  • 38.10RethrownExceptions/main.cpp
  • 38.13NoexceptSpecifier/main.cpp
  • 41.MoveSemantics/41.2LvaluesAndRvalues/main.cpp
  • 41.MoveSemantics/41.3RvalueReferences/main.cpp
  • 41.MoveSemantics/41.5MoveConstructorsMoveAssignmentOperators/main.cpp
  • 41.MoveSemantics/41.6MovingLvaluesWithStdMove/main.cpp
  • 41.MoveSemantics/41.7InvalidatePointersAfterStdMove/main.cpp
  • 41.MoveSemantics/41.08MoveOnlyTypes/main.cpp
  • 41.MoveSemantics/41.09PassingByRvalueReference/main.cpp
  • 41.MoveSemantics/main.cpp
  • 42.FunctionLikeEntities/main.cpp