第24章 高级异常处理
异常安全保证
异常安全是指当异常发生时,程序能够保持一致的状态,不会泄漏资源,也不会破坏数据结构。C++中,异常安全保证通常分为三个级别:
1. 基本异常安全(Basic Exception Safety)
- 保证:当异常发生时,程序不会泄漏资源,数据结构保持有效状态,但可能不是原始状态
- 示例:使用智能指针管理内存,确保即使发生异常也能释放内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <memory> #include <vector>
void processData(std::vector<int>& data) { std::vector<int> temp; for (int value : data) { temp.push_back(value * 2); } data.swap(temp); }
|
2. 强异常安全(Strong Exception Safety)
- 保证:当异常发生时,程序状态完全回滚到操作前的状态,就像操作从未发生过一样
- 示例:使用复制-交换技术(Copy-and-Swap)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <algorithm>
class ResourceManager { private: int* data; size_t size; public: ResourceManager(size_t s) : size(s), data(new int[s]()) {} ResourceManager(const ResourceManager& other) : size(other.size), data(new int[other.size]) { std::copy(other.data, other.data + other.size, data); } ~ResourceManager() { delete[] data; } ResourceManager& operator=(ResourceManager other) { swap(*this, other); return *this; } friend void swap(ResourceManager& a, ResourceManager& b) { std::swap(a.data, b.data); std::swap(a.size, b.size); } void resize(size_t newSize) { ResourceManager temp(newSize); std::copy(data, data + std::min(size, newSize), temp.data); swap(*this, temp); } };
|
3. 无异常保证(No-Throw Guarantee)
- 保证:操作永远不会抛出异常
- 示例:使用
noexcept说明符标记的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <vector>
void swapValues(int& a, int& b) noexcept { int temp = a; a = b; b = temp; }
template <typename T> const T& getElement(const std::vector<T>& vec, size_t index) noexcept { return vec[index]; }
|
异常传播和重新抛出
异常传播
当异常在函数中抛出但未被捕获时,它会沿着调用栈向上传播,直到找到匹配的catch块。这个过程称为栈展开(Stack Unwinding)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void func3() { throw std::runtime_error("Error in func3"); }
void func2() { func3(); }
void func1() { func2(); }
int main() { try { func1(); } catch (const std::exception& e) { std::cout << "Caught exception: " << e.what() << std::endl; } return 0; }
|
重新抛出异常
有时我们需要捕获异常,做一些处理后,再将异常重新抛出,让上层代码继续处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void processFile(const std::string& filename) { try { std::ifstream file(filename); if (!file) { throw std::runtime_error("Failed to open file"); } } catch (const std::exception& e) { std::cerr << "Error in processFile: " << e.what() << std::endl; throw; } }
int main() { try { processFile("nonexistent.txt"); } catch (const std::exception& e) { std::cout << "Main caught exception: " << e.what() << std::endl; } return 0; }
|
带修饰的重新抛出
在C++11及以后,我们可以使用std::current_exception()捕获当前异常,然后在其他上下文中重新抛出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <exception> #include <memory>
void processWithDelay() { std::exception_ptr eptr; try { throw std::runtime_error("Original error"); } catch (...) { eptr = std::current_exception(); } std::cout << "Doing other work..." << std::endl; if (eptr) { std::cout << "Rethrowing exception..." << std::endl; std::rethrow_exception(eptr); } }
int main() { try { processWithDelay(); } catch (const std::exception& e) { std::cout << "Caught exception: " << e.what() << std::endl; } return 0; }
|
异常与多线程
线程中的异常
在C++中,如果线程函数抛出未捕获的异常,程序会调用std::terminate()终止执行。因此,我们需要在每个线程函数中捕获所有可能的异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <thread> #include <iostream> #include <stdexcept>
void threadFunction() { try { throw std::runtime_error("Thread error"); } catch (const std::exception& e) { std::cerr << "Thread caught exception: " << e.what() << std::endl; } catch (...) { std::cerr << "Thread caught unknown exception" << std::endl; } }
int main() { std::thread t(threadFunction); t.join(); std::cout << "Main thread continues" << std::endl; return 0; }
|
使用std::future传递异常
std::future提供了一种在不同线程之间传递异常的机制。当线程函数抛出异常时,异常会被存储在std::future中,当调用get()时会重新抛出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <future> #include <iostream> #include <stdexcept>
int divide(int a, int b) { if (b == 0) { throw std::runtime_error("Division by zero"); } return a / b; }
int main() { std::future<int> result = std::async(std::launch::async, divide, 10, 0); try { int value = result.get(); std::cout << "Result: " << value << std::endl; } catch (const std::exception& e) { std::cout << "Caught exception: " << e.what() << std::endl; } return 0; }
|
多线程中的异常安全
在多线程环境中,异常安全变得更加复杂,因为需要考虑线程间的同步和共享数据的一致性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <thread> #include <mutex> #include <vector> #include <memory>
class ThreadSafeQueue { private: std::vector<int> data; std::mutex mtx; public: void push(int value) { std::lock_guard<std::mutex> lock(mtx); data.push_back(value); } bool pop(int& value) { std::lock_guard<std::mutex> lock(mtx); if (data.empty()) { return false; } value = data.back(); data.pop_back(); return true; } };
|
异常与RAII的深度结合
RAII与异常安全
RAII(资源获取即初始化)是实现异常安全的关键技术,它通过对象的构造和析构来管理资源,确保无论是否发生异常,资源都能被正确释放。
自定义RAII包装器
对于需要手动管理的资源,我们可以创建自定义的RAII包装器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class LockGuard { private: std::mutex& mtx; public: explicit LockGuard(std::mutex& mutex) : mtx(mutex) { mtx.lock(); } ~LockGuard() { mtx.unlock(); } LockGuard(const LockGuard&) = delete; LockGuard& operator=(const LockGuard&) = delete; };
void threadSafeOperation(std::mutex& mtx, int& sharedData, int value) { LockGuard lock(mtx); sharedData = value; }
|
智能指针与异常
智能指针是RAII的典型应用,它们确保即使发生异常,动态分配的内存也能被正确释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <memory>
void processData() { std::unique_ptr<int> data(new int[1000]); }
void processDataModern() { auto data = std::make_unique<int[]>(1000); }
|
异常处理的设计模式
1. 异常转换器模式
将底层异常转换为更高级、更具语义的异常,同时保留原始异常信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include <stdexcept> #include <string>
class DatabaseError : public std::runtime_error { public: DatabaseError(const std::string& message, int errorCode) : std::runtime_error(message), errorCode(errorCode) {} int getErrorCode() const { return errorCode; } private: int errorCode; };
class UserServiceError : public std::runtime_error { public: UserServiceError(const std::string& message, const std::exception& cause) : std::runtime_error(message + ". Cause: " + cause.what()) {} };
class UserService { private: void saveToDatabase(const std::string& userData) { throw DatabaseError("Failed to save user", 5001); } public: void createUser(const std::string& userData) { try { saveToDatabase(userData); } catch (const DatabaseError& e) { throw UserServiceError("Failed to create user", e); } } };
|
2. 异常边界模式
在模块或系统边界捕获异常,记录错误信息,然后转换为更适合外部消费者的异常或错误码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| class InternalError : public std::runtime_error { public: using std::runtime_error::runtime_error; };
enum class ApiErrorCode { SUCCESS = 0, INTERNAL_ERROR = 1, BAD_REQUEST = 2, NOT_FOUND = 3 };
struct ApiResponse { ApiErrorCode code; std::string message; std::string data; };
ApiResponse processApiRequest(const std::string& request) { try { return {ApiErrorCode::SUCCESS, "Success", "{\"result\": \"ok\"}"}; } catch (const InternalError& e) { std::cerr << "Internal error: " << e.what() << std::endl; return {ApiErrorCode::INTERNAL_ERROR, "Internal server error", ""}; } catch (const std::invalid_argument& e) { std::cerr << "Bad request: " << e.what() << std::endl; return {ApiErrorCode::BAD_REQUEST, "Invalid request", ""}; } catch (...) { std::cerr << "Unknown error" << std::endl; return {ApiErrorCode::INTERNAL_ERROR, "Internal server error", ""}; } }
|
3. 异常恢复模式
对于可恢复的错误,捕获异常后执行恢复操作,然后继续执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream> #include <stdexcept> #include <thread>
void connectToServer(const std::string& server) { throw std::runtime_error("Connection failed"); }
void processWithRetry() { const int maxRetries = 3; int retryCount = 0; while (retryCount < maxRetries) { try { std::cout << "Attempting to connect..." << std::endl; connectToServer("example.com"); std::cout << "Connection successful!" << std::endl; break; } catch (const std::runtime_error& e) { std::cout << "Error: " << e.what() << std::endl; retryCount++; if (retryCount < maxRetries) { std::cout << "Retrying in 1 second..." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); } else { std::cout << "Max retries reached. Giving up." << std::endl; throw; } } } }
|
实际项目中的异常处理策略
1. 分层异常处理
在大型项目中,通常采用分层的异常处理策略:
- 底层:抛出具体的、详细的异常
- 中层:捕获底层异常,转换为更具语义的异常,添加上下文信息
- 顶层:捕获所有异常,记录错误,执行恢复操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class DatabaseException : public std::runtime_error { public: DatabaseException(const std::string& sql, const std::string& error) : std::runtime_error("Database error: " + error + " (SQL: " + sql + ")") {} };
class OrderProcessingException : public std::runtime_error { public: OrderProcessingException(const std::string& orderId, const std::exception& cause) : std::runtime_error("Failed to process order " + orderId + ". Cause: " + cause.what()) {} };
void processOrder(const std::string& orderId) { try { } catch (const OrderProcessingException& e) { logError(e.what()); notifyUser("Order processing failed: " + e.what()); scheduleRetry(orderId); } catch (const std::exception& e) { logError("Unexpected error: " + e.what()); notifyAdmin("Unexpected error: " + e.what()); emergencyRecovery(); } }
|
2. 异常日志
良好的异常日志对于调试和监控至关重要。异常日志应该包含:
- 异常类型和消息
- 异常发生的位置(文件、行号、函数名)
- 异常发生时的上下文信息(如用户ID、请求参数等)
- 异常的调用栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <stdexcept> #include <source_location>
void logException(const std::exception& e, const std::source_location& loc = std::source_location::current()) { std::cerr << "Exception at " << loc.file_name() << ":" << loc.line() << " in " << loc.function_name() << std::endl; std::cerr << "Error: " << e.what() << std::endl; }
void processData(int value) { try { if (value < 0) { throw std::invalid_argument("Value must be non-negative"); } } catch (const std::exception& e) { logException(e); throw; } }
|
3. 异常处理与错误码的结合
在某些情况下,特别是与C API交互或需要向后兼容时,可能需要结合使用异常和错误码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| enum class ErrorCode { SUCCESS = 0, INVALID_ARGUMENT = 1, NETWORK_ERROR = 2, DATABASE_ERROR = 3 };
class AppException : public std::runtime_error { public: AppException(ErrorCode code, const std::string& message) : std::runtime_error(message), code(code) {} ErrorCode getErrorCode() const { return code; } private: ErrorCode code; };
ErrorCode processWithErrorCodes(int value, std::string& errorMessage) { try { if (value < 0) { throw AppException(ErrorCode::INVALID_ARGUMENT, "Value must be non-negative"); } return ErrorCode::SUCCESS; } catch (const AppException& e) { errorMessage = e.what(); return e.getErrorCode(); } catch (const std::exception& e) { errorMessage = e.what(); return ErrorCode::NETWORK_ERROR; } }
void processWithExceptions(int value) { if (value < 0) { throw AppException(ErrorCode::INVALID_ARGUMENT, "Value must be non-negative"); } }
|
异常处理的测试
1. 测试异常抛出
使用测试框架测试函数是否在预期条件下抛出正确的异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <stdexcept>
int divide(int a, int b) { if (b == 0) { throw std::invalid_argument("Division by zero"); } return a / b; }
TEST(DivideTest, ThrowsOnZeroDivision) { EXPECT_THROW(divide(10, 0), std::invalid_argument); }
TEST(DivideTest, ReturnsCorrectResult) { EXPECT_EQ(divide(10, 2), 5); EXPECT_EQ(divide(7, 3), 2); }
|
2. 测试异常安全
测试函数在抛出异常时是否保持异常安全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <vector>
void appendIfPositive(std::vector<int>& vec, int value) { if (value > 0) { vec.push_back(value); } }
TEST(ExceptionSafetyTest, AppendIfPositiveIsStrongExceptionSafe) { std::vector<int> original = {1, 2, 3}; std::vector<int> testVec = original; appendIfPositive(testVec, 4); EXPECT_EQ(testVec, std::vector<int>({1, 2, 3, 4})); testVec = original; appendIfPositive(testVec, 0); EXPECT_EQ(testVec, original); testVec = original; appendIfPositive(testVec, -1); EXPECT_EQ(testVec, original); }
|
3. 模拟异常
在测试中模拟异常,验证系统的恢复能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #include <stdexcept> #include <memory>
class Database { public: virtual ~Database() = default; virtual void save(const std::string& data) = 0; };
class RealDatabase : public Database { public: void save(const std::string& data) override { } };
class MockDatabase : public Database { public: MockDatabase(bool throwOnSave) : throwOnSave(throwOnSave) {} void save(const std::string& data) override { if (throwOnSave) { throw std::runtime_error("Simulated database error"); } } private: bool throwOnSave; };
class UserService { private: std::unique_ptr<Database> db; public: UserService(std::unique_ptr<Database> db) : db(std::move(db)) {} bool createUser(const std::string& userData) { try { db->save(userData); return true; } catch (const std::exception& e) { return false; } } };
TEST(UserServiceTest, HandlesDatabaseError) { auto mockDb = std::make_unique<MockDatabase>(true); UserService service(std::move(mockDb)); bool result = service.createUser("{\"name\": \"John\"}"); EXPECT_FALSE(result); }
TEST(UserServiceTest, CreatesUserSuccessfully) { auto mockDb = std::make_unique<MockDatabase>(false); UserService service(std::move(mockDb)); bool result = service.createUser("{\"name\": \"John\"}"); EXPECT_TRUE(result); }
|
C++20/23中与异常相关的新特性
1. std::source_location(C++20)
std::source_location提供了一种获取源代码位置信息的标准方法,无需使用预处理宏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <source_location> #include <string> #include <iostream>
void log(const std::string& message, const std::source_location& loc = std::source_location::current()) { std::cout << "[" << loc.file_name() << ":" << loc.line() << " in " << loc.function_name() << "] " << message << std::endl; }
void process(int value) { if (value < 0) { log("Invalid value: " + std::to_string(value)); } }
int main() { process(-1); return 0; }
|
2. std::stacktrace(C++23)
std::stacktrace提供了一种获取当前调用栈的标准方法,对于调试异常非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <stacktrace> #include <iostream> #include <stdexcept>
void logException(const std::exception& e) { std::cout << "Exception: " << e.what() << std::endl; std::cout << "Stack trace:" << std::endl; std::cout << std::stacktrace::current() << std::endl; }
void deepFunction() { throw std::runtime_error("Deep error"); }
void intermediateFunction() { deepFunction(); }
int main() { try { intermediateFunction(); } catch (const std::exception& e) { logException(e); } return 0; }
|
3. std::expected(C++23)
std::expected提供了一种返回值或错误的方式,是异常的一种替代方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <expected> #include <string> #include <iostream>
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, 2); if (result) { std::cout << "Result: " << *result << std::endl; } else { std::cout << "Error: " << result.error() << std::endl; } result = divide(10, 0); if (result) { std::cout << "Result: " << *result << std::endl; } else { std::cout << "Error: " << result.error() << std::endl; } return 0; }
|
异常处理的性能优化
1. 异常处理的性能开销
异常处理的性能开销主要来自:
- 异常表生成:编译器为每个函数生成异常表
- 栈展开:当异常抛出时,需要沿着调用栈向上查找catch块
- 异常对象的创建和销毁:异常对象通常在堆上分配
2. 性能优化策略
a. 只在特殊情况下使用异常
不要将异常用于常规的控制流,只在真正的异常情况下使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int findElement(const std::vector<int>& vec, int value) { for (size_t i = 0; i < vec.size(); i++) { if (vec[i] == value) { return i; } } throw std::runtime_error("Element not found"); }
std::optional<size_t> findElement(const std::vector<int>& vec, int value) { for (size_t i = 0; i < vec.size(); i++) { if (vec[i] == value) { return i; } } return std::nullopt; }
|
b. 使用noexcept说明符
对于不会抛出异常的函数,使用noexcept说明符,这样编译器可以进行优化。
1 2 3 4 5 6 7 8 9 10 11 12
| void swap(int& a, int& b) noexcept { int temp = a; a = b; b = temp; }
template <typename T> void swapIfNoexcept(T& a, T& b) noexcept(noexcept(std::swap(a, b))) { std::swap(a, b); }
|
c. 优化异常对象
对于自定义异常类,实现移动构造函数和移动赋值运算符,减少异常对象的复制开销。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class MyException : public std::exception { private: std::string message; public: explicit MyException(std::string msg) : message(std::move(msg)) {} MyException(MyException&& other) noexcept : message(std::move(other.message)) {} MyException& operator=(MyException&& other) noexcept { if (this != &other) { message = std::move(other.message); } return *this; } MyException(const MyException&) = delete; MyException& operator=(const MyException&) = delete; const char* what() const noexcept override { return message.c_str(); } };
|
d. 使用小型异常对象
异常对象应该尽可能小,只包含必要的信息,以减少内存分配和复制的开销。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class NetworkException : public std::runtime_error { public: NetworkException(int errorCode) : std::runtime_error(getErrorMessage(errorCode)), errorCode(errorCode) {} int getErrorCode() const { return errorCode; } private: int errorCode; static std::string getErrorMessage(int code) { switch (code) { case 404: return "Not found"; case 500: return "Internal server error"; default: return "Network error"; } } };
|
总结
高级异常处理是C++编程中的重要课题,它涉及到异常安全、异常传播、多线程中的异常、异常处理模式等多个方面。通过本章的学习,你应该掌握:
- 异常安全保证:基本异常安全、强异常安全和无异常保证
- 异常传播和重新抛出:异常如何在调用栈中传播,以及如何重新抛出异常
- 多线程中的异常:如何在多线程环境中安全地处理异常
- 异常与RAII的深度结合:如何使用RAII确保异常安全
- 异常处理的设计模式:异常转换器模式、异常边界模式、异常恢复模式
- 实际项目中的异常处理策略:分层异常处理、异常日志、异常与错误码的结合
- 异常处理的测试:测试异常抛出、测试异常安全、模拟异常
- C++20/23中与异常相关的新特性:std::source_location、std::stacktrace、std::expected
- 异常处理的性能优化:减少异常使用、使用noexcept、优化异常对象
异常处理是一把双刃剑,它既可以使程序更加健壮和可靠,也可能导致性能下降和代码复杂性增加。在实际项目中,应该根据具体情况,合理使用异常处理机制,遵循最佳实践,以达到代码质量和性能的平衡。
通过不断学习和实践,你会逐渐掌握异常处理的精髓,成为一名更加优秀的C++程序员。