본문으로 건너뛰기
Previous
Next
C++ Exception Handling Complete Guide

C++ Exception Handling Complete Guide

C++ Exception Handling Complete Guide

이 글의 핵심

Master C++ exceptions: standard hierarchy, catch-by-reference, exception safety guarantees, RAII patterns, noexcept, exceptions vs error codes, and production patterns.

Basic Exception Handling

#include <iostream>
#include <stdexcept>
using namespace std;
int divide(int a, int b) {
    if (b == 0) {
        throw runtime_error("Cannot divide by zero");
    }
    return a / b;
}
int main() {
    try {
        int result = divide(10, 0);
        cout << result << endl;
    } catch (runtime_error& e) {
        cout << "Error: " << e.what() << endl;
    }
    
    return 0;
}

Output:

Error: Cannot divide by zero

Standard Exception Hierarchy

#include <exception>
#include <stdexcept>
// Base exception
exception
// Logic errors
logic_error
  ├─ invalid_argument
  ├─ domain_error
  ├─ length_error
  └─ out_of_range
// Runtime errors
runtime_error
  ├─ range_error
  ├─ overflow_error
  └─ underflow_error

Hierarchy visualization:

graph TD
    A[std::exception] --> B[std::logic_error]
    A --> C[std::runtime_error]
    B --> D[std::invalid_argument]
    B --> E[std::out_of_range]
    B --> F[std::length_error]
    C --> G[std::range_error]
    C --> H[std::overflow_error]
    
    style A fill:#FFB6C1
    style B fill:#87CEEB
    style C fill:#90EE90

Multiple Exception Handling

try {
    // Code that may throw
} catch (out_of_range& e) {
    cout << "Out of range: " << e.what() << endl;
} catch (invalid_argument& e) {
    cout << "Invalid argument: " << e.what() << endl;
} catch (exception& e) {
    cout << "Other error: " << e.what() << endl;
} catch (...) {
    cout << "Unknown error" << endl;
}

Catch order matters: Catch specific types first, then base types. Otherwise, base type catches everything and specific handlers never execute.

Practical Examples

Example 1: File Processing Exception

#include <fstream>
#include <iostream>
#include <stdexcept>
using namespace std;
string readFile(const string& filename) {
    ifstream file(filename);
    
    if (!file.is_open()) {
        throw runtime_error("Cannot open file: " + filename);
    }
    
    string content, line;
    while (getline(file, line)) {
        content += line + "\n";
    }
    
    return content;
}
int main() {
    try {
        string content = readFile("config.txt");
        cout << content << endl;
    } catch (runtime_error& e) {
        cerr << "Error: " << e.what() << endl;
        return 1;
    }
    
    return 0;
}

Explanation: Throwing exceptions for file errors makes error handling explicit and propagates failures clearly.

Example 2: Custom Exception Class

#include <iostream>
#include <exception>
#include <string>
using namespace std;
class InvalidAgeException : public exception {
private:
    string message;
    
public:
    InvalidAgeException(int age) {
        message = "Invalid age: " + to_string(age);
    }
    
    const char* what() const noexcept override {
        return message.c_str();
    }
};
class Person {
private:
    string name;
    int age;
    
public:
    Person(string n, int a) : name(n) {
        if (a < 0 || a > 150) {
            throw InvalidAgeException(a);
        }
        age = a;
    }
    
    void print() {
        cout << name << ", " << age << " years old" << endl;
    }
};
int main() {
    try {
        Person p1("Alice", 25);
        p1.print();
        
        Person p2("Bob", 200);  // Throws exception
        p2.print();
    } catch (InvalidAgeException& e) {
        cerr << "Error: " << e.what() << endl;
    }
    
    return 0;
}

Output:

Alice, 25 years old
Error: Invalid age: 200

Explanation: Domain-specific exceptions make errors more expressive and easier to handle.

Example 3: RAII and Exception Safety

#include <iostream>
#include <memory>
using namespace std;
class Resource {
public:
    Resource() { cout << "Resource allocated" << endl; }
    ~Resource() { cout << "Resource released" << endl; }
    void use() { cout << "Resource used" << endl; }
};
void dangerousFunction() {
    throw runtime_error("Error occurred!");
}
int main() {
    try {
        // ❌ Manual management (leaks on exception)
        // Resource* r = new Resource();
        // dangerousFunction();
        // delete r;  // Never executed!
        
        // ✅ RAII (automatic cleanup)
        unique_ptr<Resource> r = make_unique<Resource>();
        r->use();
        dangerousFunction();
        // Automatically released even on exception
        
    } catch (exception& e) {
        cout << "Error: " << e.what() << endl;
    }
    
    return 0;
}

Output:

Resource allocated
Resource used
Resource released
Error: Error occurred!

Explanation: Smart pointers ensure resources are safely released even when exceptions occur.

Common Issues

Issue 1: Catching by Value

Symptom: Exception object slicing Cause: Catching by value loses derived class information Solution:

// ❌ Wrong: catch by value
try {
    throw runtime_error("error");
} catch (exception e) {  // Catch by value
    // Derived class information lost
}
// ✅ Correct: catch by reference
try {
    throw runtime_error("error");
} catch (exception& e) {  // Catch by reference
    cout << e.what() << endl;
}
// ✅ const reference (safer)
catch (const exception& e) {
    cout << e.what() << endl;
}

Key: Always catch by reference (const exception&) to avoid object slicing and preserve polymorphic behavior.

Issue 2: Throwing from Destructor

Symptom: Program abnormal termination Cause: Exception from destructor during stack unwinding calls terminate() Solution:

// ❌ Dangerous
class Resource {
public:
    ~Resource() {
        throw runtime_error("error");  // Never do this!
    }
};
// ✅ Correct
class Resource {
public:
    ~Resource() noexcept {
        try {
            // Risky operation
        } catch (...) {
            // Swallow exception
        }
    }
};

Issue 3: Exception Specification

Symptom: Warning or error when using throw() Cause: throw() is deprecated in C++11 Solution:

// ❌ Old style (deprecated)
void func() throw(int, runtime_error) {
    // ...
}
// ✅ Use noexcept
void func() noexcept {  // Doesn't throw
    // ...
}
void func2() noexcept(false) {  // May throw
    // ...
}

try-catch Basics Summary

  • try: Code block that may throw exceptions.
  • throw: Thrown type is matched to first matching catch by type. Catch derived classes before base classes to avoid slicing.
  • catch (…): Catches any type; usually placed last. Log before rethrowing to avoid silent swallow.
try {
    mayThrow();
} catch (const std::invalid_argument& e) {
    // Specific handling
} catch (const std::exception& e) {
    // Common standard exception handling
} catch (...) {
    // Unknown exceptions
}

Catch by reference: catch (const std::exception& e) is safer and more efficient than catching by value.

Exceptions vs Error Codes

AspectExceptionsError Codes (expected, optional, bool)
Control flowClean for rare failuresSuitable for frequent, expected failures
PerformanceException path is relatively expensivePredictable in hot loops
C interopDifficult to mix with C APIsWorks well with errno, return codes
VisibilityNot always obvious from signature (pre-C++17)Explicit in return type
C++23 std::expected<T, E>: Models “value or error” explicitly, enabling failure propagation without exceptions. Team convention of “exceptions only for truly exceptional failures” reduces confusion.

RAII and Exception Safety

RAII: Resources acquired in constructor, released in destructor. Even when exceptions occur, stack unwinding calls destructors, preventing leaks.

Exception Safety Guarantees

  • Basic guarantee: No resource leaks; invariants may be temporarily broken but object remains valid.
  • Strong guarantee: Commit-or-rollback (transaction-like; copy-and-swap idiom).
  • Nothrow guarantee: Operation doesn’t throw exceptions (expressible with noexcept). In production, knowing standard container exception guarantees (e.g., what vector::push_back guarantees on failure) simplifies design. For custom classes, document what happens to class invariants when copy/move assignment throws.

noexcept Deep Dive

  • Meaning: “This function doesn’t throw exceptions out.” Violation typically leads to std::terminate.
  • Move operations: std::vector uses move during reallocation if move constructor is noexcept. Adding noexcept to move constructor/assignment directly impacts performance.
  • Destructor: Destructors are implicitly noexcept. Never throw from destructors.
void swap(MyType& a, MyType& b) noexcept {
    // Only calls member swap, guarantees no exceptions
}

Conditional noexcept: noexcept(expr) form allows conditional specification.

Production Patterns

Pattern 1: Constructor Failure

Constructors have no return value, so for invalid state, throw exception (or use factory + expected).

class Database {
public:
    Database(const string& connStr) {
        if (!connect(connStr)) {
            throw runtime_error("Connection failed");
        }
    }
};

Pattern 2: Catch Only at Boundaries

Library internals propagate exceptions; catch at UI/main for logging/recovery.

// Library code: propagate
void processData() {
    if (error) throw DataException("...");
}
// Application boundary: catch
int main() {
    try {
        processData();
    } catch (const exception& e) {
        log(e.what());
        return 1;
    }
    return 0;
}

Pattern 3: std::nested_exception

Wrap low-level exceptions to preserve cause when propagating upward.

#include <exception>
#include <stdexcept>
#include <iostream>
void lowLevel() {
    throw std::runtime_error("Low-level error");
}
void midLevel() {
    try {
        lowLevel();
    } catch (...) {
        std::throw_with_nested(std::runtime_error("Mid-level error"));
    }
}
int main() {
    try {
        midLevel();
    } catch (const std::exception& e) {
        std::cout << e.what() << std::endl;
        try {
            std::rethrow_if_nested(e);
        } catch (const std::exception& nested) {
            std::cout << "  Caused by: " << nested.what() << std::endl;
        }
    }
}

Pattern 4: Testing Exceptions

Use test frameworks like Catch2 to verify exception types.

// Catch2 example
TEST_CASE("Division by zero throws") {
    REQUIRE_THROWS_AS(divide(10, 0), std::runtime_error);
}

Pattern 5: Team Convention

Establish team convention: hot loops use error codes, I/O/parsing failures use exceptions.

Best Practices

1. Catch by const Reference

// ✅ Best
catch (const std::exception& e) {
    // ...
}
// ❌ Avoid: catch by value (slicing)
catch (std::exception e) {
    // ...
}

2. Order Specific to General

try {
    // ...
} catch (const std::out_of_range& e) {
    // Specific
} catch (const std::logic_error& e) {
    // More general
} catch (const std::exception& e) {
    // Most general
} catch (...) {
    // Unknown
}

3. Don’t Swallow Silently

// ❌ Silent swallow
catch (...) {
    // Nothing—hides errors!
}
// ✅ Log and rethrow
catch (...) {
    log("Unknown exception");
    throw;  // Rethrow
}

4. Use RAII for Resources

// ✅ Smart pointers, RAII wrappers
auto file = std::make_unique<FileHandle>("data.txt");
// Automatically closed even on exception

5. noexcept for Move and Destructor

class MyClass {
public:
    MyClass(MyClass&&) noexcept;
    ~MyClass() noexcept;
};

Exception Safety Guarantees

Basic Guarantee

No resource leaks; object remains in valid (but potentially unspecified) state.

void push_back(const T& value) {
    // If exception during reallocation, no leaks
    // but vector state may change
}

Strong Guarantee

Commit-or-rollback: operation succeeds completely or leaves state unchanged.

// Copy-and-swap idiom
T& operator=(const T& other) {
    T temp(other);  // Copy
    swap(*this, temp);  // Swap (noexcept)
    return *this;
    // If copy throws, *this unchanged
}

Nothrow Guarantee

Operation never throws exceptions.

void swap(T& a, T& b) noexcept {
    // Guaranteed no exceptions
}

Exceptions vs Error Codes vs expected

Comparison Table

FeatureExceptionsError Codesstd::expected (C++23)
Control flowFor exceptional failuresFor expected failuresExplicit value-or-error
PerformanceSlow on exception pathPredictable branchingSimilar to error codes
VisibilityNot obvious in signatureExplicit in returnExplicit in return
PropagationAutomaticManual checkingManual checking
C interopDifficultEasyEasy

When to Use Each

Exceptions:

  • Rare, unexpected failures
  • Constructor failures (no return value)
  • Deep call stacks needing error propagation
  • I/O, parsing, validation errors Error Codes:
  • Frequent, expected failures (e.g., “not found”)
  • Performance-critical hot loops
  • C API interop
  • When failure is part of normal flow std::expected (C++23):
  • Explicit value-or-error modeling
  • When you want error code benefits with type safety
  • API boundaries where failure is common
// C++23 std::expected example
#include <expected>
std::expected<int, std::string> divide(int a, int b) {
    if (b == 0) {
        return std::unexpected("Division by zero");
    }
    return a / b;
}
int main() {
    auto result = divide(10, 0);
    if (result) {
        std::cout << "Result: " << *result << "\n";
    } else {
        std::cout << "Error: " << result.error() << "\n";
    }
}

RAII and Exception Safety

RAII (Resource Acquisition Is Initialization): Acquire resources in constructor, release in destructor. Stack unwinding during exception propagation calls destructors, preventing leaks.

RAII Example

#include <iostream>
#include <fstream>
class FileGuard {
    std::FILE* file_;
public:
    FileGuard(const char* path) : file_(std::fopen(path, "r")) {
        if (!file_) throw std::runtime_error("Cannot open file");
    }
    ~FileGuard() noexcept {
        if (file_) std::fclose(file_);
    }
    std::FILE* get() { return file_; }
};
void processFile(const char* path) {
    FileGuard file(path);  // RAII: auto-closes on exception
    // Use file.get()...
    // Even if exception occurs, destructor closes file
}

Exception Safety in Practice

Knowing standard container guarantees helps design:

  • vector::push_back: Strong guarantee (if reallocation fails, vector unchanged)
  • vector::reserve: Strong guarantee
  • vector::insert: Basic guarantee (may partially modify) For custom classes, document exception guarantees in copy/move operations.

Production Patterns

Pattern 1: Boundary Exception Handler

#include <iostream>
#include <exception>
int main() {
    try {
        // Application logic
        runApplication();
    } catch (const std::exception& e) {
        std::cerr << "Fatal error: " << e.what() << "\n";
        logError(e);
        return 1;
    } catch (...) {
        std::cerr << "Unknown fatal error\n";
        return 2;
    }
    return 0;
}

Pattern 2: Exception Translation

// Translate low-level exceptions to domain exceptions
void apiCall() {
    try {
        lowLevelOperation();
    } catch (const std::system_error& e) {
        throw DatabaseException("Database connection failed", e);
    }
}

Pattern 3: Scope Guard

#include <functional>
class ScopeGuard {
    std::function<void()> cleanup_;
    bool dismissed_ = false;
public:
    ScopeGuard(std::function<void()> f) : cleanup_(std::move(f)) {}
    ~ScopeGuard() noexcept {
        if (!dismissed_) {
            try {
                cleanup_();
            } catch (...) {
                // Log but don't rethrow
            }
        }
    }
    void dismiss() { dismissed_ = true; }
};
// Usage
void transactionalOperation() {
    beginTransaction();
    ScopeGuard guard([]() { rollback(); });
    
    // Operations...
    
    commit();
    guard.dismiss();  // Success: don't rollback
}

FAQ

Q1: Are exceptions slow?

A: If no exception is thrown, overhead is minimal. Exceptions are slow only when thrown and caught. For normal flow, performance impact is negligible.

Q2: When to use exceptions?

A:

  • Unrecoverable errors
  • Constructor failures
  • Deep call stack error propagation When NOT to use:
  • Normal control flow
  • Performance-critical loops

Q3: return vs throw?

A:

  • return: Normal termination, expected result
  • throw: Abnormal situation, error

Q4: Must I catch all exceptions?

A: Catch only exceptions you can handle. If you can’t handle, let it propagate upward. Catching everything and swallowing hides problems.

Q5: Why use noexcept?

A:

  • Enables compiler optimizations
  • Essential for move constructors (container optimization)
  • Clarifies intent

Q6: Exceptions vs error codes?

A:

  • Exceptions: Cleaner code, easier error propagation
  • Error codes: Performance-critical, C compatibility needed

Summary

Key Points

  1. Catch by reference: Avoid object slicing
  2. Standard hierarchy: exceptionlogic_error, runtime_error
  3. RAII: Automatic resource cleanup on exception
  4. Exception safety: Basic, strong, nothrow guarantees
  5. noexcept: For move, destructor, swap
  6. Exceptions vs codes: Choose based on failure frequency and performance needs

Exception Handling Checklist

  • Catching by const reference?
  • Specific exceptions before general?
  • RAII for all resources?
  • Destructors don’t throw?
  • Move operations marked noexcept?
  • Logging before swallowing exceptions?

  • C++ Exception Handling | try-catch-throw & Exceptions vs Error Codes
  • [Go Error Handling: Forget try-catch](/en/blog/go-error-handling-guide/
  • C++ Exception Specifications

Keywords

C++ exceptions, try-catch, throw, RAII, exception safety, noexcept, error handling One-line summary: C++ exceptions provide clean error propagation with RAII for automatic resource cleanup. Use for rare failures, error codes for frequent failures, and always catch by reference.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, exceptions, try-catch, throw, RAII, exception safety 등으로 검색하시면 이 글이 도움이 됩니다.