第26章 高级异常处理

高级异常层次结构设计

异常层次结构的设计原则

设计一个良好的异常层次结构是大型系统中异常处理的基础,它应该遵循以下原则:

  1. 单一职责原则:每个异常类应该只表示一种特定类型的错误
  2. 层次清晰:异常类应该按照错误的严重程度和类型进行层次化组织
  3. 信息丰富:异常类应该包含足够的信息,以便于调试和错误处理
  4. 可扩展性:异常层次结构应该易于扩展,以适应系统的演化

标准异常层次结构分析

C++标准库提供了一个异常层次结构,从std::exception基类派生:

  • std::exception - 所有标准异常的基类
    • std::bad_alloc - 内存分配失败
    • std::bad_cast - 类型转换失败
    • std::bad_typeid - typeid操作失败
    • std::bad_exception - 异常处理中的异常
    • std::logic_error - 逻辑错误(可在编译时检测)
      • std::domain_error - 定义域错误
      • std::invalid_argument - 无效参数
      • std::length_error - 长度错误
      • std::out_of_range - 超出范围
    • std::runtime_error - 运行时错误(只能在运行时检测)
      • std::range_error - 范围错误
      • std::overflow_error - 溢出错误
      • std::underflow_error - 下溢错误

自定义异常层次结构示例

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
69
70
71
72
73
74
75
76
77
78
#include <stdexcept>
#include <string>

// 自定义异常基类
class AppException : public std::exception {
protected:
std::string message_;
int error_code_;
std::string file_;
int line_;

public:
AppException(const std::string& msg, int code, const std::string& file, int line)
: message_(msg), error_code_(code), file_(file), line_(line) {}

const char* what() const noexcept override {
return message_.c_str();
}

int error_code() const noexcept {
return error_code_;
}

const std::string& file() const noexcept {
return file_;
}

int line() const noexcept {
return line_;
}
};

// 网络相关异常
class NetworkException : public AppException {
public:
enum NetworkError {
CONNECTION_FAILED,
TIMEOUT,
PROTOCOL_ERROR,
SERVER_ERROR
};

NetworkException(const std::string& msg, NetworkError code, const std::string& file, int line)
: AppException(msg, static_cast<int>(code), file, line) {}
};

// 数据库相关异常
class DatabaseException : public AppException {
public:
enum DatabaseError {
CONNECTION_ERROR,
QUERY_ERROR,
TRANSACTION_ERROR,
CONSTRAINT_VIOLATION
};

DatabaseException(const std::string& msg, DatabaseError code, const std::string& file, int line)
: AppException(msg, static_cast<int>(code), file, line) {}
};

// 业务逻辑异常
class BusinessLogicException : public AppException {
public:
enum BusinessError {
INVALID_OPERATION,
PERMISSION_DENIED,
RESOURCE_NOT_FOUND,
VALIDATION_ERROR
};

BusinessLogicException(const std::string& msg, BusinessError code, const std::string& file, int line)
: AppException(msg, static_cast<int>(code), file, line) {}
};

// 异常宏定义
#define THROW_NETWORK_EXCEPTION(msg, code) throw NetworkException(msg, code, __FILE__, __LINE__)
#define THROW_DATABASE_EXCEPTION(msg, code) throw DatabaseException(msg, code, __FILE__, __LINE__)
#define THROW_BUSINESS_EXCEPTION(msg, code) throw BusinessLogicException(msg, code, __FILE__, __LINE__)

异常层次结构的最佳实践

  1. 使用继承而非组合:异常类应该通过继承来建立层次关系,而不是通过组合
  2. 提供丰富的上下文信息:异常应该包含足够的信息,如错误代码、文件名、行号等
  3. 保持异常类轻量:异常类不应该包含重量级的成员,因为异常抛出时会进行拷贝
  4. 支持异常链:在C++11及以上版本,可以使用std::exception_ptr实现异常链
  5. 使用 noexcept 规范:异常类的方法应该使用noexcept修饰,以避免异常处理过程中再次抛出异常

异常传播与处理策略

异常传播机制

异常从抛出点开始,沿着调用栈向上传播,直到被捕获或导致程序终止。在传播过程中,会发生以下事情:

  1. 栈展开:销毁局部对象,按照构造顺序的逆序
  2. 异常过滤:检查每个catch块是否能够处理当前异常
  3. 异常匹配:选择最合适的catch块来处理异常

异常处理策略

1. 就地处理

在异常发生的地方直接处理,适用于可以恢复的错误情况。

1
2
3
4
5
6
7
try {
// 可能抛出异常的代码
} catch (const std::exception& e) {
// 处理异常
std::cerr << "Error: " << e.what() << std::endl;
// 恢复操作
}

2. 转换处理

将底层异常转换为更有意义的高层异常,适用于分层架构。

1
2
3
4
5
6
7
8
9
try {
// 调用底层函数,可能抛出底层异常
database_->executeQuery(query);
} catch (const std::runtime_error& e) {
// 转换为高层业务异常
throw BusinessLogicException("Database operation failed: " + std::string(e.what()),
BusinessLogicException::DATABASE_ERROR,
__FILE__, __LINE__);
}

3. 重新抛出

捕获异常后,添加额外信息并重新抛出,适用于需要添加上下文信息的场景。

1
2
3
4
5
6
7
8
try {
// 可能抛出异常的代码
} catch (const AppException& e) {
// 添加额外上下文信息
std::string enhanced_message = "Operation failed: " + std::string(e.what());
// 重新抛出增强后的异常
throw AppException(enhanced_message, e.error_code(), __FILE__, __LINE__);
}

4. 异常透明

允许异常传播到更高层次,由调用者决定如何处理,适用于无法在当前层次有效处理的情况。

1
2
3
4
5
void processData(const std::string& data) {
// 不捕获异常,让它传播到调用者
validateData(data); // 可能抛出异常
storeData(data); // 可能抛出异常
}

异常传播的最佳实践

  1. 明确异常边界:在系统的边界处捕获异常,如API接口、线程入口点等
  2. 保持异常语义:不要捕获异常后返回错误码,这会丢失异常的语义信息
  3. 避免空catch块:空catch块会吞噬异常,导致难以调试的问题
  4. 使用引用捕获:使用const&捕获异常,避免不必要的拷贝
  5. 按层次捕获:先捕获具体的异常,再捕获通用的异常

多线程与异步环境中的异常处理

线程中的异常

在多线程环境中,异常处理需要特别注意,因为线程中的未捕获异常会导致整个程序终止。

线程异常处理策略

  1. 线程内捕获:在每个线程的入口函数中捕获所有异常
  2. 异常传递:使用std::exception_ptr在线程间传递异常
  3. 线程安全的异常处理:确保异常处理过程是线程安全的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <thread>
#include <exception>
#include <stdexcept>

void threadFunction() {
try {
// 可能抛出异常的代码
throw std::runtime_error("Thread exception");
} catch (const std::exception& e) {
std::cerr << "Thread caught exception: " << e.what() << std::endl;
// 处理异常,或者重新抛出到主线程
}
}

int main() {
std::thread t(threadFunction);
t.join();
return 0;
}

异常在异步操作中的传递

在使用std::asyncstd::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
#include <iostream>
#include <future>
#include <stdexcept>

int calculate() {
throw std::runtime_error("Calculation error");
return 42;
}

int main() {
std::future<int> f = std::async(std::launch::async, calculate);

try {
int result = f.get(); // 这里会重新抛出异常
std::cout << "Result: " << result << std::endl;
} catch (const std::exception& e) {
std::cerr << "Main 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
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
69
70
71
72
73
74
75
#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <stdexcept>

class ThreadPool {
public:
ThreadPool(size_t threads) : stop(false) {
for(size_t i = 0; i < threads; ++i) {
workers.emplace_back([this] {
while(true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock, [this] {
return this->stop || !this->tasks.empty();
});
if(this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
try {
task();
} catch (const std::exception& e) {
std::cerr << "ThreadPool caught exception: " << e.what() << std::endl;
// 可以在这里添加异常处理逻辑,如记录日志等
}
}
});
}
}

template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type> {
using return_type = typename std::result_of<F(Args...)>::type;

auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);

std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task]() { (*task)(); });
}
condition.notify_one();
return res;
}

~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(std::thread &worker: workers)
worker.join();
}

private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
};

多线程异常处理的最佳实践

  1. 每个线程都要有异常处理:确保每个线程的入口函数都有try-catch块
  2. 使用std::exception_ptr传递异常:在需要在线程间传递异常时使用
  3. 线程池中的异常隔离:确保一个任务的异常不会影响其他任务
  4. 异步操作的异常处理:在调用future.get()时捕获异常
  5. 避免在析构函数中抛出异常:析构函数应该是noexcept的

高级异常安全模式

异常安全级别

异常安全分为三个级别:

  1. 基本保证:如果异常被抛出,程序不会泄漏资源,对象仍然处于有效状态,但可能不是原始状态
  2. 强保证:如果异常被抛出,程序状态保持不变(原子操作)
  3. 无抛出保证:操作绝不会抛出异常

实现强异常安全的技术

1. 拷贝-交换 idiom

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
#include <algorithm>
#include <vector>

class ResourceManager {
private:
std::vector<int>* data;

public:
ResourceManager(size_t size) : data(new std::vector<int>(size)) {}

// 拷贝构造函数
ResourceManager(const ResourceManager& other)
: data(new std::vector<int>(*other.data)) {}

// 析构函数
~ResourceManager() { delete data; }

// 拷贝-交换赋值运算符
ResourceManager& operator=(ResourceManager other) {
swap(*this, other);
return *this;
}

// 交换函数
friend void swap(ResourceManager& a, ResourceManager& b) noexcept {
std::swap(a.data, b.data);
}

// 修改数据的操作(强异常安全)
void resize(size_t newSize) {
ResourceManager temp(newSize);
std::copy(data->begin(), data->begin() + std::min(data->size(), newSize),
temp.data->begin());
swap(*this, temp); // 无异常操作
}
};

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
class Transaction {
private:
bool committed = false;
// 事务相关状态

public:
void begin() {
// 开始事务
}

void commit() {
// 提交事务
committed = true;
}

void rollback() {
// 回滚事务
}

~Transaction() {
if (!committed) {
rollback();
}
}
};

// 使用示例
void performOperation() {
Transaction tx;
tx.begin();

try {
// 执行操作,可能抛出异常
// ...
tx.commit();
} catch (...) {
// 异常会导致tx析构,自动回滚
throw;
}
}

异常中立性

异常中立性是指函数应该将异常传递给调用者,而不是捕获并处理它们,除非函数有特定的责任来处理这些异常。

1
2
3
4
5
6
7
8
// 异常中立的函数
template<typename Iterator, typename Func>
void for_each(Iterator begin, Iterator end, Func f) {
// 不捕获异常,让异常传递给调用者
for (; begin != end; ++begin) {
f(*begin); // 如果f抛出异常,会传递给for_each的调用者
}
}

异常安全的最佳实践

  1. 使用RAII管理资源:利用析构函数自动释放资源,确保即使在异常情况下也不会泄漏
  2. 优先使用标准容器和智能指针:它们已经实现了异常安全
  3. 使用拷贝-交换 idiom:实现强异常安全的赋值运算符
  4. 保持操作原子性:将复合操作分解为多个步骤,确保中间步骤的异常不会破坏状态
  5. 正确使用noexcept:对于不会抛出异常的操作使用noexcept

现代C++中的异常处理

C++11及以上的异常处理增强

1. std::exception_ptr

用于在不同上下文中传递异常。

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 <iostream>
#include <exception>
#include <stdexcept>

void handle_exception(std::exception_ptr ep) {
try {
if (ep) {
std::rethrow_exception(ep);
}
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
}

int main() {
std::exception_ptr ep;

try {
throw std::runtime_error("Test exception");
} catch (...) {
ep = std::current_exception();
}

handle_exception(ep);
return 0;
}

2. noexcept 规范

指定函数是否可能抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
// 声明为noexcept的函数
void swap(int& a, int& b) noexcept {
int temp = a;
a = b;
b = temp;
}

// 条件noexcept
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(std::swap(a, b))) {
std::swap(a, b);
}

3. 异常与移动语义

移动构造函数和移动赋值运算符应该是noexcept的,以确保在STL容器中的使用安全。

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 MyType {
private:
int* data;

public:
MyType() : data(nullptr) {}

// 移动构造函数
MyType(MyType&& other) noexcept : data(other.data) {
other.data = nullptr;
}

// 移动赋值运算符
MyType& operator=(MyType&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
return *this;
}

~MyType() {
delete data;
}
};

C++20中的异常处理

1. 概念与异常

使用概念可以在编译时检查函数是否可能抛出异常。

1
2
3
4
5
6
7
8
#include <concepts>

// 要求F是一个不抛出异常的可调用对象
template<typename F>
requires std::invocable<F> && noexcept(std::invoke(std::declval<F>()))
void safe_invoke(F&& f) {
std::invoke(std::forward<F>(f));
}

2. 协程中的异常处理

协程中的异常会被存储在协程的promise对象中,当协程被恢复时重新抛出。

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
#include <coroutine>
#include <exception>
#include <stdexcept>
#include <iostream>

struct Task {
struct promise_type {
std::exception_ptr exception;

Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}

std::suspend_never initial_suspend() noexcept {
return {};
}

std::suspend_never final_suspend() noexcept {
return {};
}

void unhandled_exception() {
exception = std::current_exception();
}

void return_void() {}
};

std::coroutine_handle<promise_type> handle;

~Task() {
if (handle) handle.destroy();
}

void resume() {
handle.resume();
if (handle.promise().exception) {
std::rethrow_exception(handle.promise().exception);
}
}
};

Task foo() {
throw std::runtime_error("Coroutine exception");
co_return;
}

int main() {
try {
auto task = foo();
task.resume();
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}

现代C++异常处理的最佳实践

  1. 使用std::exception_ptr传递异常:在需要在不同上下文中传递异常时使用
  2. 正确使用noexcept:对于不会抛出异常的操作使用noexcept,特别是移动操作
  3. 利用移动语义:移动构造函数和移动赋值运算符应该是noexcept的
  4. 结合RAII和异常:使用RAII确保资源在异常情况下的正确释放
  5. 考虑异常的性能影响:在性能关键路径上,考虑使用错误码而非异常

异常处理的性能优化

异常的性能开销

异常处理的性能开销主要来自以下几个方面:

  1. 异常表生成:编译器需要为可能抛出异常的函数生成异常表
  2. 栈展开:当异常被抛出时,需要展开栈,销毁局部对象
  3. 异常捕获:查找合适的catch块需要时间
  4. 异常对象的创建和销毁:异常对象需要在堆上分配内存

性能优化策略

1. 减少异常的使用

在性能关键路径上,考虑使用错误码而非异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 性能关键路径使用错误码
bool parseFast(const std::string& input, int& result) {
// 解析逻辑
if (/* 解析失败 */) {
return false; // 返回错误码
}
// 解析成功
result = /* 解析结果 */;
return true;
}

// 非性能关键路径使用异常
int parse(const std::string& input) {
int result;
if (!parseFast(input, result)) {
throw std::invalid_argument("Invalid input: " + input);
}
return result;
}

2. 优化异常对象

异常对象应该轻量,避免包含重量级的成员。

1
2
3
4
5
6
7
8
9
10
11
12
// 好的异常类设计
class LightweightException : public std::exception {
private:
const char* message_; // 使用const char*而非std::string

public:
explicit LightweightException(const char* msg) : message_(msg) {}

const char* what() const noexcept override {
return message_;
}
};

3. 使用noexcept减少异常表大小

对于不会抛出异常的函数,使用noexcept可以减少生成的异常表大小。

1
2
3
4
5
6
// 使用noexcept
void swap(int& a, int& b) noexcept {
int temp = a;
a = b;
b = temp;
}

4. 异常预测

在某些编译器中,可以使用__builtin_expect或类似的扩展来提示编译器异常抛出的可能性。

1
2
3
4
5
6
void process(int value) {
if (__builtin_expect(value < 0, 0)) { // 提示value < 0的可能性很小
throw std::invalid_argument("Negative value");
}
// 正常处理
}

异常处理的性能基准测试

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
#include <iostream>
#include <chrono>
#include <stdexcept>

// 使用异常的版本
int divideWithException(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero");
}
return a / b;
}

// 使用错误码的版本
bool divideWithErrorCode(int a, int b, int& result) {
if (b == 0) {
return false;
}
result = a / b;
return true;
}

int main() {
const int iterations = 10000000;
int result;

// 测试异常版本(无异常情况)
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
try {
result = divideWithException(100, i + 1);
} catch (...) {
// 不会执行
}
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Exception version (no exception): "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms" << std::endl;

// 测试错误码版本
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
divideWithErrorCode(100, i + 1, result);
}
end = std::chrono::high_resolution_clock::now();
std::cout << "Error code version: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms" << std::endl;

return 0;
}

性能优化的最佳实践

  1. 在性能关键路径上使用错误码:对于频繁调用的函数,考虑使用错误码而非异常
  2. 保持异常对象轻量:异常对象应该小而简单
  3. 使用noexcept:对于不会抛出异常的函数使用noexcept
  4. 避免在循环中抛出异常:异常应该用于异常情况,而不是正常的控制流
  5. 合理使用异常层次结构:避免过深的异常层次结构

异常处理的测试策略

异常测试的重要性

异常处理代码和正常代码一样需要测试,以确保:

  1. 异常被正确抛出:在预期的情况下抛出正确的异常
  2. 异常被正确捕获:异常能够被适当的catch块捕获
  3. 异常安全保证:在异常情况下,程序状态保持一致,资源不泄漏
  4. 异常处理路径的正确性:异常处理代码能够正确执行

异常测试技术

1. 使用断言测试异常

1
2
3
4
5
6
7
8
9
10
11
12
#include <cassert>
#include <stdexcept>

void testDivideByZero() {
bool threw = false;
try {
int result = 10 / 0; // 应该抛出异常
} catch (const std::exception&) {
threw = true;
}
assert(threw);
}

2. 使用测试框架

许多测试框架都提供了专门的宏来测试异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用Google Test框架
#include <gtest/gtest.h>

TEST(ExceptionTest, DivideByZero) {
EXPECT_THROW({
int result = 10 / 0;
}, std::exception);
}

TEST(ExceptionTest, InvalidArgument) {
EXPECT_THROW({
throw std::invalid_argument("Test");
}, std::invalid_argument);
}

3. 测试异常安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <vector>
#include <stdexcept>

void testExceptionSafety() {
std::vector<int> vec = {1, 2, 3, 4, 5};
size_t originalSize = vec.size();

try {
// 插入一个会导致异常的元素
vec.insert(vec.end(), 1000000000, 0); // 可能抛出std::bad_alloc
} catch (const std::exception&) {
// 验证vec的状态保持一致
ASSERT_EQ(vec.size(), originalSize);
ASSERT_EQ(vec, std::vector<int>({1, 2, 3, 4, 5}));
}
}

异常测试的最佳实践

  1. 测试所有异常路径:确保每个可能抛出异常的路径都被测试
  2. 测试异常类型:确保抛出的异常类型正确
  3. 测试异常信息:确保异常包含正确的错误信息
  4. 测试异常安全:确保在异常情况下程序状态保持一致
  5. 测试异常传播:确保异常能够正确传播到预期的处理点
  6. 使用专门的测试框架:利用测试框架提供的异常测试功能

实际案例分析与最佳实践

案例1:网络服务中的异常处理

问题描述

设计一个网络服务,需要处理各种网络错误、数据库错误和业务逻辑错误。

解决方案

  1. 设计异常层次结构

    • NetworkException - 网络相关错误
    • DatabaseException - 数据库相关错误
    • BusinessLogicException - 业务逻辑错误
  2. 异常处理策略

    • 在底层函数中抛出具体异常
    • 在服务接口层捕获异常,转换为适当的HTTP状态码
    • 记录异常详细信息用于调试
  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
69
70
71
72
73
74
75
76
77
78
79
80
#include <string>
#include <stdexcept>

// 异常层次结构
class ServiceException : public std::exception {
protected:
std::string message_;
int error_code_;

public:
ServiceException(const std::string& msg, int code)
: message_(msg), error_code_(code) {}

const char* what() const noexcept override {
return message_.c_str();
}

int error_code() const noexcept {
return error_code_;
}
};

class NetworkException : public ServiceException {
public:
NetworkException(const std::string& msg, int code)
: ServiceException(msg, code) {}
};

class DatabaseException : public ServiceException {
public:
DatabaseException(const std::string& msg, int code)
: ServiceException(msg, code) {}
};

class BusinessLogicException : public ServiceException {
public:
BusinessLogicException(const std::string& msg, int code)
: ServiceException(msg, code) {}
};

// 服务接口
class UserService {
public:
User getUserById(int id) {
try {
// 检查参数
if (id <= 0) {
throw BusinessLogicException("Invalid user ID", 400);
}

// 调用数据库
return database_->getUser(id); // 可能抛出DatabaseException
} catch (const DatabaseException& e) {
// 记录异常
logger_->error("Database error: {}", e.what());
// 转换为服务异常
throw ServiceException("Database error", 500);
} catch (const BusinessLogicException&) {
// 重新抛出业务逻辑异常
throw;
}
}
};

// HTTP处理程序
void handleGetUserRequest(int user_id, HttpResponse& response) {
try {
User user = userService_->getUserById(user_id);
response.setStatus(200);
response.setBody(user.toJson());
} catch (const ServiceException& e) {
response.setStatus(e.error_code());
response.setBody("{\"error\": \"" + std::string(e.what()) + "\"}");
} catch (const std::exception& e) {
// 处理未预期的异常
response.setStatus(500);
response.setBody("{\"error\": \"Internal server error\"}");
logger_->error("Unexpected error: {}", e.what());
}
}

案例2:高性能计算中的异常处理

问题描述

设计一个高性能计算库,需要在保证性能的同时处理可能的错误情况。

解决方案

  1. 分层异常处理

    • 内部计算函数使用错误码,避免异常开销
    • 公共API使用异常,提供更好的用户体验
  2. 性能优化

    • 在性能关键路径上使用错误码
    • 异常对象轻量设计
    • 使用noexcept减少异常表大小
  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
#include <stdexcept>
#include <vector>

// 内部计算函数,使用错误码
bool computeInternal(const std::vector<double>& input, std::vector<double>& output,
std::string& error_message) {
if (input.empty()) {
error_message = "Input vector is empty";
return false;
}

// 执行计算
output.resize(input.size());
for (size_t i = 0; i < input.size(); ++i) {
// 计算逻辑
output[i] = input[i] * 2.0;
}

return true;
}

// 公共API,使用异常
std::vector<double> compute(const std::vector<double>& input) {
std::vector<double> output;
std::string error_message;

if (!computeInternal(input, output, error_message)) {
throw std::invalid_argument(error_message);
}

return output;
}

// 高性能版本,使用noexcept
void computeFast(const std::vector<double>& input, std::vector<double>& output) noexcept {
// 假设输入已经验证过,不会抛出异常
output.resize(input.size());
for (size_t i = 0; i < input.size(); ++i) {
output[i] = input[i] * 2.0;
}
}

异常处理的最佳实践总结

  1. 设计合理的异常层次结构

    • 从std::exception派生
    • 按照错误类型和严重程度分层
    • 包含足够的错误信息
  2. 选择合适的异常处理策略

    • 就地处理:对于可以恢复的错误
    • 转换处理:对于分层架构中的异常
    • 重新抛出:对于需要添加上下文信息的异常
    • 异常透明:对于无法在当前层次处理的异常
  3. 确保异常安全

    • 使用RAII管理资源
    • 实现强异常安全(拷贝-交换idiom)
    • 正确使用noexcept
  4. 考虑性能影响

    • 在性能关键路径上使用错误码
    • 保持异常对象轻量
    • 避免在循环中抛出异常
  5. 测试异常处理

    • 测试所有异常路径
    • 测试异常类型和信息
    • 测试异常安全保证
  6. 多线程环境中的异常处理

    • 每个线程都要有异常处理
    • 使用std::exception_ptr传递异常
    • 线程池中的异常隔离
  7. 现代C++特性的应用

    • 使用std::exception_ptr传递异常
    • 正确使用noexcept
    • 移动构造函数和移动赋值运算符应该是noexcept的
    • 结合RAII和异常

通过遵循这些最佳实践,可以构建更加健壮、可靠、高性能的C++应用程序,同时保持代码的清晰性和可维护性。