C++ 异常处理与泛型编程总结
一、异常处理
⭐ C++ 异常机制概述
cpp
try {
if (denominator == 0)
throw std::invalid_argument("Division by zero");
result = numerator / denominator;
}
catch (const std::invalid_argument& e) {
std::cerr << "Invalid: " << e.what() << "\n";
}
catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
}
catch (...) {
std::cerr << "Unknown exception\n";
}
⭐ 标准异常类层次
⭐ 自定义异常
cpp
class HttpException : public std::runtime_error {
int status_code_;
public:
HttpException(int code, const std::string& msg)
: std::runtime_error(msg), status_code_(code) {}
int StatusCode() const { return status_code_; }
};
class NotFoundException : public HttpException {
public:
explicit NotFoundException(const std::string& resource)
: HttpException(404, "Not found: " + resource) {}
};
// 使用
try {
throw NotFoundException("/api/user/123");
} catch (const HttpException& e) {
std::cerr << e.StatusCode() << ": " << e.what() << "\n";
}
⭐ noexcept 详解
cpp
// 声明函数不会抛出异常
void SafeFunc() noexcept {
// 如果内部抛出异常 → 直接 std::terminate()
}
// 条件 noexcept
template <typename T>
void Swap(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
// 检查表达式是否 noexcept
static_assert(noexcept(42 + 1)); // true
static_assert(!noexcept(std::string("hi"))); // false(可能抛 bad_alloc)
为什么 noexcept 很重要?
⭐ 规则:移动构造函数、移动赋值、析构函数、swap 都应该标记
noexcept。
⭐ 异常安全等级
| 等级 | 名称 | 保证 | 示例 |
|---|---|---|---|
| 无保证 | No guarantee | 可能泄漏/破坏状态 | 不推荐 |
| 基本保证 | Basic | 不泄漏资源,对象处于有效状态 | 大多数 STL 操作 |
| 强保证 | Strong | 要么成功,要么状态不变(事务语义) | vector::push_back |
| 不抛保证 | Nothrow | 绝不抛出异常 | 析构函数、swap |
cpp
// 强异常保证示例(copy-and-swap idiom)
class Widget {
std::vector<int> data_;
public:
Widget& operator=(Widget other) { // 值传递(拷贝)
swap(*this, other); // noexcept swap
return *this; // other 析构旧资源
}
friend void swap(Widget& a, Widget& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
}
};
异常 vs 错误码
| 特性 | 异常 | 错误码 |
|---|---|---|
| 不可忽略 | ✅ 不处理则崩溃 | ❌ 可以忽略返回值 |
| 正常路径性能 | ✅ 零开销 | 需要每次检查 |
| 异常路径性能 | ❌ 较慢(栈展开) | ✅ 快 |
| 代码可读性 | ✅ 正常流程清晰 | 嵌套 if/else |
| 跨层传播 | ✅ 自动传播 | 需要层层传递 |
| 适用场景 | 真正的异常情况 | 可预见的错误 |
cpp
// C++23 std::expected(错误码的现代替代)
#include <expected>
std::expected<int, std::string> Divide(int a, int b) {
if (b == 0) return std::unexpected("Division by zero");
return a / b;
}
auto result = Divide(10, 0);
if (result) {
std::cout << *result;
} else {
std::cout << result.error(); // "Division by zero"
}
二、泛型编程(模板高级特性)
⭐ 模板的编译模型
模板在编译时实例化(不是运行时),每种类型生成独立的函数/类代码。
⭐ 函数模板的重载解析
cpp
// 通用版本
template <typename T>
void Print(T val) { std::cout << "Template: " << val << "\n"; }
// 特化版本(针对 const char*)
template <>
void Print(const char* val) { std::cout << "Specialized: " << val << "\n"; }
// 非模板重载
void Print(int val) { std::cout << "Non-template: " << val << "\n"; }
Print(42); // Non-template(非模板优先)
Print(3.14); // Template(匹配通用模板)
Print("hello"); // Specialized(匹配特化版本)
解析优先级:非模板重载 > 模板特化 > 通用模板
⭐ 类模板的偏特化
cpp
// 通用版本
template <typename T, typename U>
class Pair {
T first_; U second_;
};
// 偏特化:两个类型相同
template <typename T>
class Pair<T, T> {
T first_; T second_;
// 可以提供不同实现
};
// 偏特化:第二个类型为指针
template <typename T, typename U>
class Pair<T, U*> {
T first_; U* second_;
// 可以特殊处理指针
};
Pair<int, double> p1; // 通用版本
Pair<int, int> p2; // 偏特化版本(T==T)
Pair<int, double*> p3; // 偏特化版本(U*)
⭐ type_traits 类型萃取
cpp
#include <type_traits>
// 编译期类型判断
static_assert(std::is_integral_v<int>); // true
static_assert(std::is_floating_point_v<double>); // true
static_assert(std::is_pointer_v<int*>); // true
static_assert(std::is_same_v<int, int32_t>); // true
// 类型变换
using T1 = std::remove_const_t<const int>; // int
using T2 = std::remove_reference_t<int&>; // int
using T3 = std::add_pointer_t<int>; // int*
using T4 = std::decay_t<const int&>; // int
// 条件类型
using T5 = std::conditional_t<true, int, double>; // int
using T6 = std::conditional_t<false, int, double>; // double
⭐ SFINAE 与 enable_if
cpp
// 只接受整数类型
template <typename T>
std::enable_if_t<std::is_integral_v<T>, T>
SafeAdd(T a, T b) {
// 检测溢出
if (a > 0 && b > std::numeric_limits<T>::max() - a)
throw std::overflow_error("Integer overflow");
return a + b;
}
// 只接受浮点类型
template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
SafeAdd(T a, T b) {
return a + b; // 浮点不检查溢出
}
SafeAdd(1, 2); // 调用整数版本
SafeAdd(1.0, 2.0); // 调用浮点版本
// SafeAdd("a", "b"); // ❌ 编译错误(SFINAE 排除)
⭐ C++20 Concepts
cpp
#include <concepts>
// 定义概念
template <typename T>
concept Printable = requires(T t) {
{ std::cout << t } -> std::same_as<std::ostream&>;
};
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
// 使用概念约束
template <Number T>
T Add(T a, T b) { return a + b; }
// 或者用 requires 子句
template <typename T>
requires Printable<T>
void Print(const T& val) {
std::cout << val << "\n";
}
// 最简写法(C++20 缩写模板)
void Log(Printable auto const& msg) {
std::cout << "[LOG] " << msg << "\n";
}
Concepts vs SFINAE:
| 特性 | SFINAE / enable_if | Concepts(C++20) |
|---|---|---|
| 可读性 | ❌ 复杂难懂 | ✅ 清晰自然 |
| 错误信息 | ❌ 模板错误海洋 | ✅ 明确的约束违反提示 |
| 组合性 | ❌ 嵌套困难 | ✅ && ` |
| 推荐 | C++20 前使用 | C++20 首选 |
三、编译期计算
⭐ constexpr 编译期计算
cpp
// constexpr 函数:编译期或运行期都可以执行
constexpr int Factorial(int n) {
if (n <= 1) return 1;
return n * Factorial(n - 1);
}
constexpr int val = Factorial(10); // 编译期计算:3628800
int arr[Factorial(5)]; // ✅ 可以用作数组大小
int runtime_n = GetInput();
int result = Factorial(runtime_n); // 运行期计算也可以
// C++17 constexpr if
template <typename T>
auto Convert(T t) {
if constexpr (std::is_integral_v<T>) {
return static_cast<double>(t);
} else if constexpr (std::is_same_v<T, std::string>) {
return std::stod(t);
} else {
return t;
}
}
⭐ consteval 和 constinit(C++20)
cpp
// consteval:必须在编译期执行(立即函数)
consteval int Square(int n) { return n * n; }
constexpr int a = Square(5); // ✅ 编译期
// int b = Square(runtime_n); // ❌ 编译错误!必须编译期
// constinit:保证变量在编译期初始化(但不是 const)
constinit int global = 42; // 编译期初始化
// constinit int bad = GetVal(); // ❌ 不能运行期初始化
void Foo() {
global = 100; // ✅ 可以修改(不是 const)
}
四、序列化
⭐ C++ 中的序列化方案
| 方案 | 性能 | 可读性 | 跨语言 | 适用场景 |
|---|---|---|---|---|
| Protocol Buffers | ⭐⭐⭐ | ❌ | ✅ | RPC、高性能通信 |
| FlatBuffers | ⭐⭐⭐⭐ | ❌ | ✅ | 游戏、嵌入式 |
| JSON | ⭐ | ✅✅ | ✅ | REST API、配置 |
| MessagePack | ⭐⭐ | ❌ | ✅ | 紧凑传输 |
JSON 序列化示例(nlohmann/json)
cpp
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// 结构体序列化
struct User {
std::string name;
int age;
std::vector<std::string> hobbies;
};
// 自定义序列化
void to_json(json& j, const User& u) {
j = json{{"name", u.name}, {"age", u.age}, {"hobbies", u.hobbies}};
}
void from_json(const json& j, User& u) {
j.at("name").get_to(u.name);
j.at("age").get_to(u.age);
j.at("hobbies").get_to(u.hobbies);
}
// 使用
User user{"Alice", 30, {"reading", "coding"}};
json j = user; // 自动序列化
std::string s = j.dump(2); // 格式化输出
User user2 = json::parse(s).get<User>(); // 反序列化
五、C++ 反射(有限支持)
当前状态
C++ 没有原生运行时反射(不像 Java),但有以下替代方案:
cpp
// RTTI 示例
#include <typeinfo>
class Base { virtual ~Base() = default; };
class Derived : public Base {};
Base* p = new Derived();
std::cout << typeid(*p).name(); // 输出 Derived 的类型名
if (auto* d = dynamic_cast<Derived*>(p)) {
// 安全向下转型
}
六、最佳实践总结
⭐ 异常处理最佳实践
| 规则 | 说明 |
|---|---|
按 const 引用捕获 | catch (const std::exception& e) |
| 先捕获子类 | 派生异常在基类异常前面 |
| 析构中不抛异常 | 标记为 noexcept |
| 用 RAII 保证资源安全 | 异常时自动释放 |
移动操作标 noexcept | 让 STL 使用移动而非拷贝 |
| 不要用异常做流程控制 | 只在真正异常情况使用 |
⭐ 模板编程最佳实践
| 规则 | 说明 |
|---|---|
| 模板定义放头文件 | 保证每个翻译单元可见 |
| 使用 Concepts(C++20) | 替代 SFINAE,错误信息更好 |
constexpr 优先 | 能编译期计算就编译期 |
if constexpr 替代 SFINAE | 更直观的条件编译 |
| 避免模板过度嵌套 | 控制编译时间和错误信息 |
七、总结知识图谱
📑 文章目录
💬 评论