3More Effective C++

异常

异常处理

异常安全异常保证nothrow异常层次

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第3章:异常(Exceptions)

一、导读

1.1 本章学习目标

第3章讲解 C++ 异常处理的最佳实践。Meyers 在此给出了关于异常安全、异常保证、异常层次等方面的重要建议。

本章核心学习目标:

  • 掌握异常安全的三个层次
  • 理解异常保证:noexcept 的使用
  • 掌握异常层次的设计
  • 理解异常与构造函数的关系

1.2 为什么本章如此重要

异常处理是 C++ 中最容易出错的部分。错误的异常处理会导致资源泄漏、程序崩溃、难以调试的问题。

1.3 核心问题引导

为什么析构函数不应该抛出异常?

异常安全的强保证如何实现?

什么时候应该用 noexcept?


二、核心概念详解

2.1 析构函数不应抛出异常

cpp
// 错误: 析构函数抛出异常
class BadConnection {
    int fd;
public:
    ~BadConnection() {
        if (close(fd) != 0)
            throw std::runtime_error("close failed");  // 危险!
    }
};

// 如果 vector<BadConnection> 在扩容时析构旧元素抛出异常
// → std::terminate 被调用 → 程序崩溃

// 正确: 在析构函数中处理异常
class GoodConnection {
    int fd;
public:
    ~GoodConnection() noexcept {
        try {
            if (close(fd) != 0) {
                // 记录日志,但不抛出
                logError("close failed");
            }
        } catch (...) {
            // 吞掉异常,保证析构不抛出
        }
    }
    
    void close() {  // 提供显式的关闭方法
        if (close(fd) != 0)
            throw std::runtime_error("close failed");
    }
};

2.2 异常安全保证

cpp
// 基本保证: 异常后对象处于有效状态
void basicGuarantee(std::vector<int>& v, int value) {
    v.push_back(value);  // 如果抛出 bad_alloc,v 仍然有效
}

// 强保证: 异常后状态不变(如同操作未发生)
void strongGuarantee(std::vector<int>& v, int value) {
    auto backup = v;  // 先备份
    try {
        v.push_back(value);
    } catch (...) {
        v = std::move(backup);  // 回滚
        throw;
    }
}

// 更好的实现: copy-and-swap
void strongGuaranteeBetter(std::vector<int>& v, int value) {
    auto copy = v;
    copy.push_back(value);
    v.swap(copy);  // swap 不抛出异常
}

// noexcept 的使用
void swap(Widget& other) noexcept {
    std::swap(data, other.data);
}

// noexcept 的好处:
// 1. 编译器可以生成更高效的代码
// 2. STL 容器在移动时优先使用 noexcept 的移动操作

2.3 异常层次设计

cpp
// 设计异常层次
class AppException : public std::exception {
protected:
    std::string message;
public:
    explicit AppException(std::string msg) : message(std::move(msg)) {}
    const char* what() const noexcept override { return message.c_str(); }
};

class DatabaseException : public AppException {
public:
    using AppException::AppException;
};

class ConnectionException : public DatabaseException {
public:
    using DatabaseException::DatabaseException;
};

class QueryException : public DatabaseException {
public:
    using DatabaseException::DatabaseException;
};

// 使用:
try {
    db.execute("SELECT ...");
} catch (const ConnectionException& e) {
    // 处理连接错误
} catch (const QueryException& e) {
    // 处理查询错误
} catch (const DatabaseException& e) {
    // 处理其他数据库错误
} catch (const AppException& e) {
    // 处理其他应用错误
} catch (const std::exception& e) {
    // 处理标准库异常
}

三、实战案例

3.1 案例一:异常安全的资源管理

cpp
class Transaction {
    Database& db;
    bool committed = false;
public:
    explicit Transaction(Database& d) : db(d) {
        db.execute("BEGIN");
    }
    
    void execute(const std::string& sql) {
        db.execute(sql);  // 如果抛出异常,析构函数回滚
    }
    
    void commit() {
        db.execute("COMMIT");
        committed = true;
    }
    
    ~Transaction() noexcept {
        if (!committed) {
            try {
                db.execute("ROLLBACK");
            } catch (...) {
                // 析构函数不抛出
            }
        }
    }
};

// 使用:
void transferMoney(Database& db, int from, int to, int amount) {
    Transaction txn(db);
    txn.execute("UPDATE accounts SET balance = balance - " + 
                std::to_string(amount) + " WHERE id = " + std::to_string(from));
    txn.execute("UPDATE accounts SET balance = balance + " + 
                std::to_string(amount) + " WHERE id = " + std::to_string(to));
    txn.commit();
}  // 如果任何操作抛出异常,txn 析构时自动回滚

四、本章小结

析构函数不应抛出异常,否则可能导致程序崩溃

异常安全分三个层次:基本保证、强保证、不抛保证

noexcept 标记不抛出的函数,帮助编译器优化

异常层次应该反映错误的语义分类


五、思考与练习

思考题

为什么 STL 容器要求元素的移动操作是 noexcept 的?

如何实现异常安全的容器操作?

编程练习

实现一个异常安全的数据库事务类。

设计一个异常层次,覆盖文件操作、网络操作、数据库操作。


3/6异常
0%