Skip to content

Latest commit

 

History

History
224 lines (173 loc) · 11.5 KB

constexpr-exceptions.md

File metadata and controls

224 lines (173 loc) · 11.5 KB

constexpr Exceptions


Executive Abstract

constexpr Exceptions refers to the C++26 language capability (standardized via proposal P3032) that permits exception throwing (throw) and exception handling (try-catch) to execute completely within compile-time constant evaluation. Building on C++20's syntactic inclusion of try-catch blocks, C++26 enables exception objects to be thrown, caught, and handled inside the compiler's constant expression interpreter. This allows error-recovery logic, bounds-checked container methods (std::vector::at), and domain validation algorithms to execute at compile time while guaranteeing that uncaught exceptions halt compilation with informative diagnostic error backtraces.


Motivation & Historical Context

Pre-C++26 Deficiencies: The Uncaught Exception Barrier

C++20 (via proposal P1002R1) allowed try and catch blocks to appear syntactically inside constexpr functions.

However, C++20 and C++23 suffered from a major operational limitation in constant evaluation:

// C++20/C++23 Constant Evaluation Limitation
constexpr int divide(int a, int b) {
    try {
        if (b == 0) throw std::invalid_argument("Division by zero!"); // Throws exception
        return a / b;
    } catch (const std::invalid_argument&) {
        return -1; // C++20/23: THIS CATCH BLOCK WAS NEVER EXECUTED IN CONSTEXPR!
    }
}

constexpr int res = divide(10, 0); // COMPILE ERROR in C++20/C++23!

Deficiencies of C++20/C++23 constexpr Exception Rules:

  1. Immediate Compilation Failure on throw: In C++20 and C++23, evaluating a throw statement during constant evaluation caused compilation to halt immediately, even if the throw statement was enclosed inside an active try-catch block! The compiler's constant interpreter was incapable of unwinding compile-time stack frames or executing catch handlers.
  2. Inability to Test Error Recovery at Compile Time: Standard containers (std::vector, std::optional) and parsing libraries could not validate exception-recovery logic inside static_assert or constexpr unit tests.

Solution: C++26 proposal P3032 updated [expr.const], instructing the compiler's constant expression interpreter to construct exception objects in compile-time memory, unwind compile-time call stacks, and execute matching catch handlers.


Technical Mechanics & Execution Protocol

1. Compile-Time Exception Handling Protocol ([expr.const])

When a throw expression is evaluated during constant evaluation:

$$\text{constexpr Execution } \longrightarrow \mathbf{\text{throw } \text{ExceptionObject;}} \quad \longrightarrow \quad \text{Interpreter searches matching } \mathbf{\text{catch}} \text{ block in constexpr stack frame}$$

                Execution inside constexpr Function
                                 │
                                 ▼
                     Evaluates 'throw Exception;'
                                 │
         ┌───────────────────────┴───────────────────────┐
         ▼                                               ▼
MATCHING CATCH BLOCK FOUND?                     NO CATCH BLOCK (UNCAUGHT EXCEPTION)
• Executes catch(const Exception&) block        • Constant Evaluation HALTS!
• Resumes normal constexpr execution            • Compiler emits diagnostic error
• static_assert / constexpr SUCCEEDS!            displaying exception type & message!
  1. Caught Exception Path: If a matching catch handler exists within the constant evaluation call stack, the compiler transfers execution to the catch block. Constant evaluation resumes normally and can yield a valid constant expression result.
  2. Uncaught Exception Path: If an exception escapes the outermost constant evaluation boundary without being caught, constant evaluation fails, and the compiler emits a diagnostic build error containing the uncaught exception's type name and message.

Evolution Across Standards (C++20 to C++26/C++29)

┌──────────────────────────────────────────────────────────────────┐
│ C++20 / C++23                                                    │
├──────────────────────────────────────────────────────────────────┤
│ • try-catch blocks permitted syntactically inside constexpr (P1002)│
│ • Evaluating 'throw' caused IMMEDIATE compile-time error          │
│ • Catch handlers NEVER executed during constant evaluation        │
└──────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ C++26                                                            │
├──────────────────────────────────────────────────────────────────┤
│ • Introduced full constexpr Exception Handling (P3032)           │
│ • Throwing and catching exceptions supported inside constexpr    │
│ • Enables compile-time testing of exception recovery paths       │
└──────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ C++29 (Candidate / Future)                                       │
├──────────────────────────────────────────────────────────────────┤
│ • Static/constexpr exception optimization refinements             │
│ • Extended exception propagation across compile-time reflection   │
└──────────────────────────────────────────────────────────────────┘

Comprehensive Code Examples

Example 1: Throwing and Catching Exceptions inside Compile-Time Evaluation (P3032)

Demonstrates catching a thrown exception inside a constexpr function during static_assert.

#include <iostream>
#include <stdexcept>

// C++26 constexpr function with fully functional try-catch exception handling!
constexpr int safe_parse_digit(char c) {
    try {
        if (c < '0' || c > '9') {
            throw std::out_of_range("Character is not a decimal digit!"); // Throws exception in constexpr!
        }
        return c - '0';
    } catch (const std::out_of_range&) {
        // C++26: Catch handler EXECUTED AT COMPILE TIME!
        return -1; // Fallback error value
    }
}

int main() {
    // Evaluated entirely at COMPILE TIME!
    constexpr int digit = safe_parse_digit('7');  // Returns 7
    constexpr int error = safe_parse_digit('X');  // Throws out_of_range, caught by catch block -> Returns -1!

    static_assert(digit == 7, "Valid digit parsing failed");
    static_assert(error == -1, "Compile-time exception catching failed");

    std::cout << "Compile-time exception throwing and catching verified successfully.\n";
    return 0;
}

Example 2: Validating Container Bounds Protection (std::vector::at) in static_assert

Demonstrates testing container bounds-checking exceptions during compilation.

#include <iostream>
#include <vector>
#include <stdexcept>

constexpr bool verify_vector_bounds_exception() {
    std::vector<int> numbers = {10, 20, 30};

    try {
        // Access index 5 (Out of bounds!)
        int val = numbers.at(5); // std::vector::at throws std::out_of_range!
        (void)val;
        return false;
    } catch (const std::out_of_range&) {
        // Caught exception successfully during constexpr evaluation!
        return true;
    }
}

int main() {
    // Verified at COMPILE TIME!
    static_assert(verify_vector_bounds_exception() == true,
                  "Compile-time vector bounds exception test failed!");

    std::cout << "Compile-time container exception handling verified.\n";
    return 0;
}

Example 3: Uncaught Compile-Time Exception Diagnostic Output

Demonstrates how an uncaught exception in a constexpr context halts compilation with an informative error message.

#include <iostream>
#include <stdexcept>

constexpr int strict_divide(int a, int b) {
    if (b == 0) {
        // Uncaught exception during constant evaluation!
        throw std::domain_error("Fatal Error: Division by zero in constant expression!");
    }
    return a / b;
}

int main() {
    constexpr int valid_div = strict_divide(10, 2); // OK: Evaluates to 5

    // UNCOMMENTING BELOW HALTS COMPILATION WITH DIAGNOSTIC ERROR:
    // constexpr int invalid_div = strict_divide(10, 0);
    // COMPILER ERROR: Constant evaluation failed!
    // Uncaught exception of type 'std::domain_error': "Fatal Error: Division by zero..."

    std::cout << "Valid Division Result: " << valid_div << "\n";
    return 0;
}

Engineering Best Practices & Common Pitfalls

  1. Use constexpr Exceptions for Compile-Time Validation & Testing:

    • Write constexpr unit tests using static_assert to verify that invalid inputs to your custom container or parser types correctly throw expected exception types.
  2. Uncaught Exceptions Serve as Compile-Time Assertions:

    • Intentionally allowing an exception to escape an un-handled constexpr function is an effective mechanism for creating custom domain-specific compile-time assertions. The compiler will halt compilation and print the exception message in the build log.
  3. Ensure Exception Classes Support Constant Evaluation:

    • Custom exception types thrown inside constexpr functions must be literal types or satisfy constexpr constructor requirements.

Relationships


Back to C++26 section.