Skip to content

Instantly share code, notes, and snippets.

@MangaD
Last active September 30, 2025 03:32
Show Gist options
  • Save MangaD/8c22b388733b2d0b734969e58c94f58c to your computer and use it in GitHub Desktop.
Save MangaD/8c22b388733b2d0b734969e58c94f58c to your computer and use it in GitHub Desktop.
Stack Unwinding & Exception Propagation in C++

πŸ“Œ Stack Unwinding & Exception Propagation in C++

CC0

Disclaimer: ChatGPT generated document.

Stack unwinding is the process of cleaning up the stack when an exception is thrown. It involves destructing local variables and calling cleanup functions while propagating the exception up the call stack until a matching catch block is found.


πŸ”Ή 1. What Happens When an Exception is Thrown?

When an exception is thrown:

  1. The normal flow of execution stops.
  2. The runtime searches for a matching catch block.
  3. Stack unwinding occurs:
    • Local variables are destroyed.
    • Destructors of objects go out of scope.
    • The call stack unwinds until a matching catch is found.
  4. If no catch is found, std::terminate() is called.

πŸ“Œ Example of Exception Propagation

#include <iostream>

void func3() {
    throw std::runtime_error("Error!");  // Exception is thrown
}

void func2() {
    func3();  // No try-catch here β†’ Unwinds further
}

void func1() {
    func2();  // No try-catch here β†’ Unwinds further
}

int main() {
    try {
        func1();  // βœ… Catch block is here
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << "\n";
    }
}

πŸ“Œ Output

Caught exception: Error!

βœ” The exception propagates up the call stack until catch handles it.


πŸ”Ή 2. Stack Unwinding Explained

βœ… 2.1 What is Stack Unwinding?

Stack unwinding cleans up the stack when an exception occurs. Each function in the call chain exits in reverse order, and destructors for local objects are automatically called.

πŸ“Œ Example: Destructors Running During Stack Unwinding

#include <iostream>

struct Obj {
    std::string name;
    Obj(std::string n) : name(n) { std::cout << "Constructing " << name << "\n"; }
    ~Obj() { std::cout << "Destroying " << name << "\n"; }
};

void func3() {
    Obj obj3("func3");
    throw std::runtime_error("Error!");
}

void func2() {
    Obj obj2("func2");
    func3();
}

void func1() {
    Obj obj1("func1");
    func2();
}

int main() {
    try {
        func1();
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << "\n";
    }
}

πŸ“Œ Output

Constructing func1
Constructing func2
Constructing func3
Destroying func3  // βœ… Stack unwinding starts here
Destroying func2
Destroying func1
Caught exception: Error!

βœ” Objects in the stack are destroyed in reverse order (LIFO - Last In, First Out).
βœ” This ensures that resources are properly released when an exception occurs.


βœ… 2.2 What Happens if a Destructor Throws an Exception?

🚨 Throwing an exception inside a destructor during stack unwinding leads to program termination!

πŸ“Œ Example: Dangerous Destructor

#include <iostream>

class Bad {
public:
    ~Bad() {
        std::cout << "Bad destructor!\n";
        throw std::runtime_error("Destructor error!");  // ❌ BAD PRACTICE!
    }
};

void func() {
    Bad bad;
    throw std::runtime_error("Initial error");
}

int main() {
    try {
        func();
    } catch (...) {
        std::cout << "Caught exception\n";
    }
}

πŸ“Œ Output

Bad destructor!
terminate called after throwing an instance of 'std::runtime_error'
Aborted (core dumped)

βœ” C++ does not allow two active exceptions at once!
βœ” If an exception occurs during stack unwinding, std::terminate() is called.

πŸ“Œ Solution: Catch the Exception Inside the Destructor

~Bad() {
    try {
        throw std::runtime_error("Destructor error!");
    } catch (...) {
        std::cout << "Exception in destructor caught internally\n";
    }
}

βœ” Now the program won’t crash if the destructor fails.


πŸ”Ή 3. Exception Propagation

When an exception is thrown, it bubbles up the call stack until:

  1. A catch block handles it.
  2. The program terminates if no handler is found.

πŸ“Œ Example: Multiple catch Blocks

#include <iostream>

void func() {
    throw std::runtime_error("Runtime error");
}

int main() {
    try {
        func();
    } catch (const std::logic_error& e) {
        std::cout << "Caught logic error\n";  // ❌ This won't match
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << "\n";  // βœ… This matches
    } catch (...) {
        std::cout << "Caught unknown exception\n";
    }
}

πŸ“Œ Output

Caught exception: Runtime error

βœ” C++ matches the most specific exception type first.
βœ” If no matching catch is found, it moves to a more generic one (std::exception).
βœ” If no catch block matches, std::terminate() is called.


πŸ”Ή 4. Best Practices for Exception Handling & Stack Unwinding

βœ… 4.1 Use RAII (Resource Acquisition Is Initialization)

RAII ensures resources are automatically released when they go out of scope.

πŸ“Œ Example: Managing Resources with RAII

#include <iostream>
#include <fstream>

class File {
    std::ofstream file;
public:
    File(const std::string& filename) {
        file.open(filename);
        if (!file) throw std::runtime_error("Failed to open file");
    }
    ~File() { file.close(); }
};

void func() {
    File f("data.txt");  // βœ… Automatically closes file if an exception occurs
    throw std::runtime_error("Error!");
}

int main() {
    try {
        func();
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << "\n";
    }
}

βœ” RAII prevents memory leaks by ensuring that destructors are always called.


βœ… 4.2 Avoid Exception-Safe Destructors

  • Destructors must never throw exceptions.
  • Always catch exceptions inside destructors.

πŸ“Œ Example

~MyClass() noexcept {
    try {
        // Cleanup code that might fail
    } catch (...) {
        // Log the error instead of rethrowing
    }
}

βœ… 4.3 Use noexcept for Performance

Functions marked as noexcept cannot throw exceptions, improving optimization.

πŸ“Œ Example

void fastFunc() noexcept {
    // Compiler optimizes function call
}

βœ” Use noexcept for functions that should never throw exceptions.
βœ” Avoid marking destructors as noexcept if they might throw.


πŸ“Œ Summary

Concept Explanation
Stack Unwinding The process of cleaning up stack frames when an exception is thrown.
Exception Propagation The bubbling up of an exception until a matching catch block is found.
Destructor Behavior Destructors are automatically called during stack unwinding.
Throwing in Destructors 🚨 Bad practice! Throws in destructors cause program termination.
RAII Best practice for managing resources safely.
noexcept Performance optimization for functions that won’t throw.

πŸš€ Next Steps

Would you like:

1️⃣ A deep dive into how exception handling is implemented in assembly?
2️⃣ How compilers optimize exception handling?
3️⃣ Alternatives to exceptions, like std::expected?


πŸ“Œ Comprehensive Deep Dive into C++ Exception Handling: Assembly, Compiler Optimizations, and Alternatives

This guide will cover everything related to exception handling in C++, including:

βœ… How exception handling works at the assembly level
βœ… How compilers optimize exception handling
βœ… Alternatives to exceptions (std::expected, error codes, etc.)


πŸ”Ή 1. How C++ Exception Handling Works at the Assembly Level

C++ exception handling is different from C's setjmp/longjmp mechanism. Instead of storing registers and jumping back, C++ uses a structured exception-handling model that works with:

  1. Exception Tables (Metadata for stack unwinding)
  2. EH (Exception Handling) Runtime
  3. Stack Unwinding Mechanism

βœ… 1.1 What Happens When an Exception is Thrown?

πŸ“Œ Steps

  1. throw statement executes
    • The C++ runtime creates an exception object.
    • The compiler stores exception-handling metadata.
  2. Stack Unwinding Starts
    • Destructors for local variables are automatically called.
    • The compiler searches exception tables for a matching catch block.
  3. Matching catch Found
    • Execution jumps to the catch block.
    • The exception is handled, and execution resumes.
  4. If No Handler Exists
    • The program calls std::terminate() and exits.

βœ… 1.2 C++ Exception Handling in Assembly (GCC/Clang)

Let's analyze compiled assembly code for exception handling.

πŸ“Œ C++ Code

#include <iostream>

void foo() {
    throw std::runtime_error("Error!"); 
}

int main() {
    try {
        foo();
    } catch (const std::exception& e) {
        std::cout << "Caught: " << e.what() << "\n";
    }
}

πŸ“Œ GCC Assembly (-O2 Optimization)

foo():
    mov edi, OFFSET FLAT:.LC0  ; Load exception message
    call _ZNSt13runtime_errorC1EPKc ; Call std::runtime_error constructor
    call __cxa_throw  ; Call runtime function to throw exception

main():
    call foo
    jmp .L1
.L1:
    call __cxa_begin_catch  ; Start handling the exception
    mov rdi, rax
    call std::cout << e.what()
    call __cxa_end_catch  ; End catch block

βœ” __cxa_throw is used to throw exceptions.
βœ” __cxa_begin_catch starts catching the exception.
βœ” __cxa_end_catch marks the end of the catch block.


βœ… 1.3 Exception Handling on Windows (MSVC)

Windows uses Structured Exception Handling (SEH) with RaiseException().

πŸ“Œ Windows Assembly (MSVC)

foo():
    mov rcx, OFFSET FLAT:"Error!"
    call std::runtime_error
    call _CxxThrowException

main():
    call foo
    jmp .L1
.L1:
    call __CxxFrameHandler3  ; Windows exception handler

βœ” MSVC uses _CxxThrowException instead of __cxa_throw.
βœ” Exception tables are generated in PE format.


πŸ”Ή 2. How Compilers Optimize Exception Handling

Because exceptions introduce overhead, compilers optimize exception handling by:

  1. Table-Based EH (Zero-cost model)
  2. SJLJ (Setjmp/Longjmp)
  3. Code-Based EH (Windows SEH)

βœ… 2.1 Table-Based Exception Handling (Zero-cost EH)

This model does not add extra instructions in normal execution.

πŸ“Œ How it Works

  • The compiler stores exception metadata in separate tables.
  • At runtime, tables are consulted only when an exception is thrown.
  • Used by GCC/Clang (Itanium ABI).

βœ” Pros:

βœ… No performance overhead unless an exception occurs
βœ… Smaller code size

βœ” Cons:

❌ More complex stack unwinding
❌ Larger binary size due to exception tables


βœ… 2.2 SJLJ (Setjmp/Longjmp) Exception Handling

Used in older GCC versions and embedded systems.

πŸ“Œ How it Works

  • Instead of metadata tables, the compiler manually saves CPU registers.
  • If an exception occurs, it jumps back using longjmp.

βœ” Pros:

βœ… Faster stack unwinding (compared to tables)
βœ… Works in non-stack-based environments

βœ” Cons:

❌ Slower execution (adds setjmp calls even if no exceptions occur)
❌ Can cause memory leaks if resources aren’t cleaned up properly


βœ… 2.3 Code-Based Exception Handling (Windows SEH)

Microsoft's Structured Exception Handling (SEH) embeds exception metadata directly into the executable.

βœ” Pros:

βœ… Integrated with Windows debugging tools
βœ… More predictable behavior

βœ” Cons:

❌ Less flexible than Itanium ABI


πŸ”Ή 3. Alternatives to Exceptions

Since exceptions have runtime overhead, modern C++ provides alternatives.

βœ… 3.1 Using std::expected (C++23)

Instead of throwing exceptions, return an expected value or an error.

πŸ“Œ Example

#include <expected>
#include <iostream>

std::expected<int, std::string> divide(int a, int b) {
    if (b == 0) return std::unexpected("Divide by zero");
    return a / b;
}

int main() {
    auto result = divide(10, 0);
    if (!result) {
        std::cout << "Error: " << result.error() << "\n";
    }
}

βœ” Avoids the overhead of stack unwinding.


βœ… 3.2 Using Error Codes

Instead of throwing exceptions, return integer error codes.

πŸ“Œ Example

int divide(int a, int b, int& result) {
    if (b == 0) return -1;  // Error code
    result = a / b;
    return 0;
}

int main() {
    int output;
    if (divide(10, 0, output) == -1) {
        std::cout << "Error: Division by zero!\n";
    }
}

βœ” No exception handling required.


βœ… 3.3 Using std::optional

For functions that might fail, return std::optional instead of throwing.

πŸ“Œ Example

#include <optional>
#include <iostream>

std::optional<int> divide(int a, int b) {
    if (b == 0) return std::nullopt;
    return a / b;
}

int main() {
    auto result = divide(10, 0);
    if (!result) {
        std::cout << "Error: Division by zero!\n";
    }
}

βœ” More readable than error codes.


πŸ“Œ Summary

Feature Advantages Disadvantages
C++ Exceptions Automatic cleanup, works with RAII Slower, requires stack unwinding
std::expected (C++23) No stack unwinding, safer More verbose syntax
Error Codes No exceptions, simple Manual error checking required
std::optional Safer than error codes Can’t store error messages

πŸš€ Next Steps

Would you like:

1️⃣ A deeper dive into std::expected and functional error handling?
2️⃣ How different compilers implement exception handling in assembly?
3️⃣ How noexcept improves performance?


πŸ“Œ Deep Dive into C++ Exception Handling: std::expected, Compiler Implementations, and noexcept Optimization

This guide covers:

βœ… How std::expected works and how to use it for functional error handling
βœ… How different compilers implement exception handling at the assembly level
βœ… How noexcept affects performance and compiler optimizations


πŸ”Ή 1. std::expected: Functional Error Handling Without Exceptions

std::expected<T, E> (introduced in C++23) is a new standard library feature that provides an alternative to exceptions for error handling. It allows functions to return either a valid result (T) or an error (E).

βœ… 1.1 Why Use std::expected?

Approach Pros Cons
Exceptions (throw) Cleaner code, automatic stack unwinding Expensive, requires RTTI
Error codes (int) No exceptions, works everywhere Harder to maintain, no strong typing
std::optional<T> Cleaner than error codes Cannot store error messages
std::expected<T, E> Best of both worlds! Returns either valid result or error Slightly more verbose syntax

βœ… 1.2 How std::expected Works

πŸ“Œ Example: Using std::expected for Safe Division

#include <expected>
#include <iostream>

std::expected<int, std::string> divide(int a, int b) {
    if (b == 0) return std::unexpected("Division by zero error");
    return a / b;
}

int main() {
    auto result = divide(10, 0);
    if (!result) {  // βœ… Check if there's an error
        std::cout << "Error: " << result.error() << "\n";
    } else {
        std::cout << "Result: " << *result << "\n";
    }
}

πŸ“Œ Output

Error: Division by zero error

βœ” Avoids expensive stack unwinding.
βœ” Encourages explicit error checking.


βœ… 1.3 Chaining Operations With std::expected

πŸ“Œ Example: Using and_then to Chain Operations

std::expected<int, std::string> half(int x) {
    if (x % 2 != 0) return std::unexpected("Not divisible by 2");
    return x / 2;
}

int main() {
    auto result = divide(10, 2)
        .and_then(half)  // βœ… Chaining safe operations
        .and_then(half);

    if (!result) {
        std::cout << "Error: " << result.error() << "\n";
    } else {
        std::cout << "Final Result: " << *result << "\n";
    }
}

βœ” Functional-style error handling, without exceptions.


πŸ”Ή 2. How Different Compilers Implement Exception Handling

Different compilers have different exception-handling implementations. C++ compilers generate metadata tables and use runtime functions for stack unwinding.


βœ… 2.1 GCC/Clang: Itanium ABI (Table-Based Exception Handling)

GCC and Clang use the Itanium ABI, which relies on exception tables instead of inserting exception-handling code into normal execution paths.

πŸ“Œ C++ Code

void foo() {
    throw std::runtime_error("Error!");
}

int main() {
    try {
        foo();
    } catch (const std::exception& e) {
        std::cout << "Caught: " << e.what() << "\n";
    }
}

πŸ“Œ Generated Assembly (-O2 Optimized)

foo():
    mov edi, OFFSET FLAT:.LC0  ; Load exception message
    call _ZNSt13runtime_errorC1EPKc ; Call std::runtime_error constructor
    call __cxa_throw  ; Calls GCC exception throw function

main():
    call foo
    jmp .L1
.L1:
    call __cxa_begin_catch
    call std::cout << e.what()
    call __cxa_end_catch

βœ” __cxa_throw is responsible for throwing the exception.
βœ” __cxa_begin_catch and __cxa_end_catch handle catching and cleanup.
βœ” Table-based EH has *zero cost* unless an exception occurs.


βœ… 2.2 MSVC: Windows SEH (Structured Exception Handling)

Microsoft's implementation uses Structured Exception Handling (SEH).

πŸ“Œ Windows Assembly (MSVC)

foo():
    mov rcx, OFFSET FLAT:"Error!"
    call std::runtime_error
    call _CxxThrowException

main():
    call foo
    jmp .L1
.L1:
    call __CxxFrameHandler3  ; Windows exception handler

βœ” Windows EH integrates with system debugging tools (e.g., minidumps).
βœ” Less flexible than Itanium ABI but works well with OS-level exception handling.


πŸ”Ή 3. How noexcept Affects Performance

The noexcept specifier tells the compiler that a function will not throw exceptions, allowing for better optimizations.


βœ… 3.1 When to Use noexcept

Scenario Should Use noexcept? Why?
Move constructors βœ… Yes Avoids unnecessary exception checks
Destructor (~T()) βœ… Yes Allows compiler optimizations
Performance-critical functions βœ… Yes Compiler generates better code
Functions that might throw ❌ No Misleading and unsafe

πŸ“Œ Example: Marking Functions noexcept

void fastFunc() noexcept {
    // βœ… Compiler optimizes this function
}

βœ… 3.2 How noexcept Improves Compiler Optimizations

πŸ“Œ Without noexcept

mov eax, [rsp+8]  ; Load function parameter
cmp eax, 0
jne throw_error   ; Check if an exception should be thrown

πŸ“Œ With noexcept

mov eax, [rsp+8]  ; Load function parameter

βœ” Avoids unnecessary exception-handling code.


βœ… 3.3 noexcept and Move Semantics

std::vector and other containers check if a type’s move constructor is noexcept. If not, they use a copy constructor instead, reducing performance.

πŸ“Œ Example: Performance Impact

struct NoExceptMove {
    NoExceptMove(NoExceptMove&&) noexcept {};  // βœ… noexcept move
};

struct RegularMove {
    RegularMove(RegularMove&&) {};  // ❌ No noexcept
};

int main() {
    std::vector<NoExceptMove> v1(10);
    std::vector<RegularMove> v2(10);
}

βœ” v1 uses std::move() because move is noexcept.
βœ” v2 uses std::copy() instead of move, reducing performance.


πŸ“Œ Summary

Feature Explanation
std::expected<T, E> Functional error handling, no stack unwinding
Itanium ABI (GCC/Clang) Uses table-based exception handling (zero-cost model)
MSVC Exception Handling Uses Windows SEH (structured exception handling)
noexcept Improves compiler optimizations and move semantics

βœ” Use std::expected for predictable error handling.
βœ” Use noexcept for performance-critical functions.
βœ” Understand compiler optimizations to write efficient C++ code.


πŸš€ Next Steps

Would you like:

1️⃣ A deep dive into how compilers optimize noexcept and inline functions?
2️⃣ How std::expected compares to std::optional and std::variant?
3️⃣ Assembly-level comparisons between Itanium ABI and Windows SEH?


πŸ“Œ Does std::expected Deprecate Exceptions?

With C++23, std::expected was introduced as a better alternative to std::optional and error codes, making error handling more explicit and structured. However, it does NOT deprecate exceptions, nor does it replace them entirely.

This guide covers:

βœ… What std::expected is and how it works
βœ… How it compares to exceptions
βœ… Pros and cons of std::expected vs. exceptions
βœ… When to use std::expected and when to use exceptions


πŸ”Ή 1. What is std::expected?

std::expected<T, E> is a return-type-based error-handling mechanism where:

  • T is the expected (successful) value.
  • E is the error value (usually an std::string, std::error_code, or custom type).

βœ… 1.1 Example: Using std::expected

#include <iostream>
#include <expected>
#include <string>

std::expected<int, std::string> safe_divide(int a, int b) {
    if (b == 0) return std::unexpected("Division by zero");
    return a / b;  // Return expected value
}

int main() {
    auto result = safe_divide(10, 0);

    if (result) {
        std::cout << "Result: " << *result << std::endl;
    } else {
        std::cout << "Error: " << result.error() << std::endl;
    }
}

πŸ“Œ Output:

Error: Division by zero

βœ” No exceptions are thrown!
βœ” Errors are explicitly handled.


πŸ”Ή 2. Does std::expected Replace Exceptions?

🚨 No! std::expected is NOT a replacement for exceptions.
Instead, std::expected is an alternative for handling recoverable errors, whereas exceptions are better suited for unexpected, exceptional failures.

βœ… 2.1 Key Differences

Feature std::expected<T, E> Exceptions (throw)
Error Propagation Explicit return value Implicit via stack unwinding
Performance Zero-cost (no runtime overhead) Can be slow (requires stack unwinding)
Code Complexity Requires checking result manually Cleaner, automatic handling
Use Case Expected failures (e.g., file I/O) Exceptional failures (e.g., memory corruption)

βœ… 2.2 Example: Using Exceptions vs. std::expected

πŸ“Œ Using Exceptions

#include <iostream>
#include <stdexcept>

int safe_divide(int a, int b) {
    if (b == 0) throw std::runtime_error("Division by zero");
    return a / b;
}

int main() {
    try {
        int result = safe_divide(10, 0);
        std::cout << "Result: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cout << "Error: " << e.what() << std::endl;
    }
}

πŸ“Œ Output:

Error: Division by zero

βœ” Automatic error propagation using throw/catch.
βœ” No need to manually check for errors.
🚨 However, exceptions introduce performance overhead (stack unwinding).


πŸ“Œ Using std::expected

#include <iostream>
#include <expected>
#include <string>

std::expected<int, std::string> safe_divide(int a, int b) {
    if (b == 0) return std::unexpected("Division by zero");
    return a / b;
}

int main() {
    auto result = safe_divide(10, 0);
    if (result) {
        std::cout << "Result: " << *result << std::endl;
    } else {
        std::cout << "Error: " << result.error() << std::endl;
    }
}

πŸ“Œ Output:

Error: Division by zero

βœ” Explicit error handling without stack unwinding.
βœ” Better performance for frequent failures.
🚨 Requires manually checking return values.


πŸ”Ή 3. When to Use std::expected vs. Exceptions

βœ… Use std::expected When:

βœ” Failures are expected and frequent
βœ” Performance is critical (e.g., game loops, embedded systems)
βœ” You want explicit error handling (e.g., file I/O, network operations)
βœ” You’re writing a library that should not use exceptions

πŸ“Œ Example: File Handling

std::expected<std::string, std::string> read_file(const std::string& path) {
    std::ifstream file(path);
    if (!file) return std::unexpected("Failed to open file");
    
    std::string content((std::istreambuf_iterator<char>(file)),
                         std::istreambuf_iterator<char>());
    return content;
}

βœ… Use Exceptions When:

βœ” Failures are rare but serious
βœ” You need automatic stack unwinding
βœ” Error propagation should be implicit
βœ” The error is unexpected and unrecoverable (e.g., out of memory)

πŸ“Œ Example: Memory Allocation

void allocate_large_array() {
    int* arr = new int[1000000000];  // ❌ Throws std::bad_alloc if allocation fails
}

πŸ”Ή 4. Performance Comparison

Metric std::expected<T, E> Exceptions (throw)
Error Checking Overhead βœ… None (explicit return value) ❌ Slow (stack unwinding)
Branch Prediction ❌ Worse (requires if checks) βœ… Better (implicit propagation)
Code Size βœ… Smaller (no exception tables) ❌ Larger (exception tables, stack metadata)

βœ” std::expected is zero-cost when no error occurs.
βœ” Exceptions incur runtime overhead due to stack unwinding.


πŸ“Œ Summary

Feature std::expected<T, E> Exceptions (throw)
Explicit Error Handling βœ… Yes ❌ No
Automatic Propagation ❌ No βœ… Yes
Performance Overhead βœ… None ❌ Stack unwinding is costly
Ease of Use ❌ Requires if checks βœ… Implicit flow

πŸš€ Key Takeaways:

βœ… std::expected is great for expected failures (file I/O, networking, parsing).
βœ… Exceptions are better for unexpected failures (memory corruption, logic errors).
βœ… They are complementary, not replacements.


πŸš€ Next Steps

Would you like:

1️⃣ Performance benchmarks: std::expected vs. exceptions?
2️⃣ Real-world use cases of std::expected in C++ projects?
3️⃣ Alternatives like std::variant, std::optional, and error codes?

Let me know what interests you most! 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment