CppCon 2014 学习:Exception-Safe Coding
以下是你提到的内容(例如 “Exception-Safe Coding 理解” 和 “Easier to Read!” 等)翻译成中文并进一步解释:
承诺:理解异常安全(Exception-Safe Coding)
什么是异常安全?
异常安全是指:即使程序在运行过程中抛出了异常,代码也能保持程序的正确性、不泄露资源,并且处于一致的状态。
优点(重新整理并翻译)
更易阅读(Easier to Read!)
遵循异常安全的编程风格,代码结构更清晰、更简洁,例如使用 RAII(资源获取即初始化)或智能指针,使代码一目了然。
更易理解与维护(Easier to Understand and Maintain!)
明确的资源管理方式让代码行为可预测,异常发生时不需要担心内存泄漏或状态混乱,更容易调试与维护。
更易编写(Easier to Write!)
现代 C++ 提供了许多辅助工具(如 std::unique_ptr
、std::vector
、std::optional
),可以轻松实现异常安全,比手动管理内存更简单。
没有性能损耗(No time penalty!)
使用 RAII 和标准容器,编译器会自动优化资源管理,不会带来额外的性能负担。
100% 稳定性(100% Robust)
遵循异常安全原则,代码具备高度健壮性:
- 不会泄漏资源
- 即使异常抛出,程序也不会处于错误或不一致的状态
异常安全的等级(C++ 中常见的 4 个级别)
- 不抛异常保证(No-throw guarantee):函数承诺永远不会抛出异常。
- 强保证(Strong guarantee):发生异常时,程序状态保持不变。
- 基本保证(Basic guarantee):即使发生异常,也不会泄漏资源,但状态可能会改变。
- 无保证(No guarantee):函数在抛出异常后不保证任何行为 —— 应尽量避免。
总结:为什么要学习异常安全?
异常安全不仅是防止程序崩溃,更是为了写出稳定、易维护、现代化的 C++ 程序。
这段代码片段讲的是如何正确检查系统调用或库函数的错误,尤其是用 errno
来标记和处理错误。让我帮你详细解释一下这段代码和它背后的思路:
Error Flagging(错误标记)理解
errno = 0; // 先将 errno 清零
old_nice = getpriority(PRIO_PROCESS, 0); // 调用系统函数获取优先级
/* check errno */
if (errno) // 检查 errno 是否被设置(非零表示出错)
{/* handle error */ // 处理错误的逻辑
}
关键点解释:
- errno = 0;
在调用getpriority
之前,先把errno
清零。
这是因为很多系统调用在失败时会设置errno
,但如果调用成功,errno
的值通常不会自动清零。因此,先清零是为了保证后续判断errno
时不会受之前错误的影响。 - old_nice = getpriority(PRIO_PROCESS, 0);
调用系统函数getpriority
,获取当前进程的优先级。
这个函数的返回值可能是合法的优先级,也可能是 -1,表示出错。但这里直接用errno
来判断是否出错更准确。 - if (errno)
调用后判断errno
是否非零。
如果非零,说明上一步调用发生错误,程序需要进入错误处理逻辑。
为什么这样写?
- 有些系统调用返回的错误值可能与正常返回值重叠,比如
getpriority
函数在失败时返回 -1,但 -1 也可能是合法值(优先级范围内)。 - 直接检查返回值是否为 -1 可能会误判。
- 所以 官方建议先清零 errno,再调用函数,最后检查 errno 是否被设置,判断是否出错更准确。
简单总结
- 调用系统函数之前先将
errno
清零,避免之前的错误影响判断。 - 调用后通过检查
errno
判断是否出错。 - 如果
errno
被设置(非零),说明调用失败,必须处理错误。
errno
方式做错误标记(Error Flagging)的一些缺点和问题。
使用 Error Flagging(错误标记)方法的问题
1. 错误容易被忽略!
errno
是全局变量,默认情况下程序不会自动处理或响应错误。- 程序员如果忘记检查
errno
,错误就会悄悄地被忽略,导致程序异常行为或隐藏的 bug。
2. 不确定到底是哪个函数调用失败!
- 如果代码中调用了多个系统函数,但只检查了最后的
errno
,你无法明确知道是哪个调用真正出错了。 - 因为
errno
是全局状态,后续的调用可能覆盖之前的错误代码,造成模糊和混乱。
3. 代码难读难写!
- 要反复写
errno=0
、检查errno
、错误处理,代码变得臃肿和繁琐。 - 逻辑被异常处理细节分散,影响代码的可读性和维护性。
总结
虽然用 errno
方式能检测错误,但它有不少缺点,主要是:
- 容易被忽视的错误
- 错误来源不明确
- 代码可读性差
这也是为什么现代 C++ 更多推荐用异常处理(try-catch)或返回值封装(比如std::optional
、std::expected
)来更优雅地管理错误。
你这段内容讲的是用**返回码(Return Codes)**进行错误处理的方式,我帮你整理成更易懂的中文解释:
返回码(Return Codes)错误处理理解
1. 返回值就是错误/状态码!
- 几乎所有的 API 函数都会通过返回值告诉调用者:操作成功还是失败,或者当前的状态是什么。
- 这些返回值通常是整数类型(
int
、long
),代表不同的错误或状态。
2. 一组已知的错误/状态码!
- 这些返回码都是预先定义好的,比如:
0
代表成功- 其他值代表各种错误类型,比如文件不存在、权限不足、内存不足等
- 调用者可以根据返回码进行对应的处理。
3. 错误码沿调用链向上传递!
- 当某个函数调用另一个函数时,如果底层函数返回错误码,通常会被上层函数传递上去。
- 这样错误信息就沿着调用链层层传递,最终能被最高层处理。
总结
- 返回码是传统且广泛使用的错误处理方法。
- 它通过函数的返回值告知调用者操作结果。
- 需要调用者主动检查返回值,进行错误处理。
不过,这种方式也有缺点,比如容易忘记检查返回码,代码可读性较差。现代 C++更推荐用异常机制或者封装类型来管理错误。
返回码(Return Code)错误处理方法存在的问题,我帮你用中文整理并解释:
返回码方式存在的问题
1. 错误容易被忽略
- 程序默认不会自动处理返回码中的错误,如果程序员不主动检查,错误就会被忽略,导致程序可能在错误状态下继续运行。
2. 错误链容易断裂,错误丢失
- 调用链上的某个函数如果没有正确返回错误码(“断开链条”),那么之前的错误信息就会丢失,
- 这会导致调用者无法感知到底哪里出现了问题。
3. 代码难读难写
- 程序中需要频繁写很多判断返回码的代码,
- 导致代码变得冗长、难以维护,逻辑混乱。
4. 异常机制解决了这些问题
- 使用异常(Exception)可以自动传播错误,避免“断链”问题。
- 代码结构更清晰,错误处理与正常逻辑分离,更易读、更健壮。
总结
- 返回码方式需要开发者“手动管理”错误,容易出错且代码不优雅。
- 异常处理机制是对返回码方式的改进,自动传播错误并简化代码。
返回码(Return Code)错误处理方式的问题以及异常处理的优缺点:
返回码方式的问题
1. 错误容易被忽略
- 返回码的错误如果不主动检查,默认会被忽视。
- 程序可能在出现错误后继续执行,导致隐蔽的bug。
2. 错误链“断裂”导致错误丢失
- 调用链中如果某个函数没有正确返回错误码,
- 之前发生的错误可能就被“丢弃”了,最终调用者拿不到错误信息。
3. 代码冗长,难读难写
- 需要大量的
if (error) {...}
判断, - 代码变得繁琐,逻辑被错误检查分散,影响可维护性。
异常处理的优势
- 异常可以自动传播错误,不容易丢失。
- 错误处理逻辑与正常代码分离,使代码更简洁易读。
- 异常机制天然避免“断链”问题。
异常处理的缺点
- 但异常机制本身也有缺点和挑战:
- 可能影响性能(尤其是在大量抛捕异常的场景)
- 编写异常安全代码需要一定技巧
- 不当使用异常可能导致难以调试的问题
总结
返回码方式 | 异常处理方式 |
---|---|
错误易被忽略 | 自动传播错误,不易遗漏 |
错误丢失(断链)风险 | 错误传递更可靠 |
代码冗长,错误处理分散 | 代码更简洁,逻辑更清晰 |
简单直观,但易出错 | 更复杂,需要掌握异常安全写法 |
这句话是用幽默且形象的方式,强调了错误处理不当会带来严重后果。我帮你用中文解释一下: |
“黑暗面”——错误处理不当的恶果
Broken error handling leads to bad states,
错误处理不完善或失败,
bad states lead to bugs,
会让程序进入错误、混乱的状态,
bugs lead to suffering.
这些错误状态最终导致各种难以排查和修复的程序缺陷(bug),
给开发者和用户带来痛苦和麻烦。
—— 尤达大师(Yoda)
理解总结:
- 错误处理不严谨 是程序出错的根源之一。
- 一旦程序状态错乱,后续问题层出不穷,甚至难以挽回。
- 因此,良好的错误处理机制至关重要,是写出健壮、可靠软件的基础。
这段代码是用异常安全的方式实现了一个赋值操作符 operator=
,并且用了**“自我销毁后原地重构”(destroy and reconstruct in place)**的技巧。让我帮你逐行解释,并讲解它背后的思想:
代码讲解:异常安全的赋值操作符
T& T::operator=(T const& x)
{if (this != &x) // 1. 自赋值保护(防止 x 和 *this 是同一个对象){this->~T(); // 2. 销毁当前对象内容(析构)new (this) T(x); // 3. 在当前内存位置“原地”构造新对象,拷贝 x}return *this; // 4. 返回自身引用,支持链式赋值
}
详细解释:
1. 自赋值检测
if (this != &x)
- 检查是不是给自己赋值,避免不必要的销毁和重构操作。
2. 销毁当前对象(析构)
this->~T();
- 调用当前对象的析构函数,清理当前对象所占资源(比如动态内存、文件句柄等)。
- 这一步确保不会泄漏资源。
3. 原地重构(placement new)
new (this) T(x);
- 使用**定位 new(placement new)**在当前对象的内存地址重新构造一个对象,调用拷贝构造函数。
- 这样做实现了“先销毁后重建”,确保对象状态完全由
x
的副本决定。
4. 返回自身
return *this;
- 符合赋值操作符的惯例,返回赋值后的对象自身引用,支持连续赋值如
a = b = c;
。
为什么这样写?(异常安全考虑)
- 传统赋值实现可能先修改当前对象,再复制资源,如果拷贝过程中抛异常,可能导致对象处于半坏状态。
- 这里先销毁再构造,如果拷贝构造抛异常,当前对象已经销毁,异常传播出去后对象不存在,但不会处于不一致状态。
- 这是**强异常安全保证(Strong exception safety)**的一种实现。
注意点
- 这种写法假设析构和构造是正确的异常安全。
- 适合实现了正确析构和拷贝构造的类。
- 如果类有复杂成员,确保拷贝构造是异常安全的。
早期程序员对异常(exceptions)的抵触情绪,以及如今异常机制的成熟情况和当前仍存在的顾虑。
“黑暗面”——早期对异常的抵触与现状
1. 早期采用者不愿意接受异常机制
- 早期的 C++ 开发者对异常机制比较抗拒,可能因为:
- 实现不完善
- 性能开销大
- 代码复杂难以调试
2. 实现问题已经基本解决了!
- 如今主流编译器对异常的支持已经非常成熟:
- 可靠(异常处理不会导致程序崩溃或资源泄漏)
- 高性能(优化良好,开销已经大幅降低)
- 可移植(跨平台表现一致)
3. 现在真正令人担忧的是什么?
- 虽然技术实现上没大问题,开发者仍然关心:
- 如何写出异常安全的代码(比如避免异常导致资源泄漏或状态不一致)
- 异常控制流可能增加代码复杂度
- 团队对异常使用的规范和一致性
- 异常和性能敏感代码(比如内核、嵌入式系统)是否适用
总结
- 异常机制的技术障碍已经被克服,现代编译器和运行时环境非常支持异常。
- 但开发者的心态和编程习惯仍然是推广异常的主要“瓶颈”。
- 对异常安全设计和代码规范的深入理解,才是今天最值得关注的。
错误处理对程序控制流程的影响,
代码路径中断(Code Path Disruption)
- 如果存在不能被忽视的错误情况,
- 意味着我们调用的函数其实在返回时可能会包含错误信息(错误码或异常),
- 这些错误返回虽然“看不见”(代码里不明显),但必须被正确处理。
更具体地说:
- 当函数有可能失败时,它的调用路径不再是简单的“调用-返回”那样线性顺畅,
- 而是可能因为错误而打断正常执行流程,进入错误处理逻辑。
- 这种“隐藏的错误返回”改变了程序的控制流,使代码更复杂,也更难以维护。
总结:
- 不能忽略的错误条件会导致函数返回“隐形”的错误结果,
- 需要程序员在代码中显式处理这些错误,避免错误被遗漏。
- 这也说明了错误处理是程序流程设计的重要部分,不能被轻视。
《Exception Handling: A False Sense of Security》(异常处理:一种错误的安全感):
主要内容和观点
- 异常处理并非万能
- 虽然异常机制设计来提升程序健壮性,但在实际复杂的模板类和泛型代码中,异常安全仍然难以保证。
- 代码可能因为异常引发不一致状态,导致难以预料的错误。
- 异常安全的复杂性
- 实现真正的异常安全代码很复杂,特别是在使用模板和动态内存管理时。
- 即使是表面看起来正确的异常处理,也可能隐藏缺陷。
- 没有简单解决方案
- 文章指出,异常处理存在固有的挑战和局限。
- 对于复杂数据结构和泛型代码,没有“一劳永逸”的异常处理方案。
- 需要深入理解异常安全的设计原则和细节,编写代码时必须仔细考虑异常可能带来的影响。
- 在设计复杂类时,除了异常处理,还要考虑其它手段(如RAII、智能指针、事务性操作等)来保障程序的健壮性。
这段代码是Cargill文章中分析的那个模板栈(Stack<T>
)里的pop()
函数,出现了异常处理相关的问题。让我帮你详细解释:
代码讲解 — Stack<T>::pop()
template <class T>
T Stack<T>::pop()
{if (top < 0)throw "pop on empty stack"; // 栈空时抛出异常return v[top--]; // 返回栈顶元素,并将栈顶索引减一
}
逐行理解
if (top < 0)
判断栈是否为空(top
是栈顶索引,负值表示空栈)。throw "pop on empty stack";
如果栈空,则抛出异常,字符串字面量作为异常对象。return v[top--];
返回当前栈顶元素,然后栈顶指针减一。
Cargill指出的问题
- 异常安全问题
- 返回类型是值类型
T
,这里发生返回时,涉及对象的拷贝构造。 - 如果
T
的拷贝构造函数抛异常,top--
已经执行了吗? - 实际上,
top--
是后缀递减,先取值再减,所以顺序是先返回v[top]
的副本,再减top
。 - 但如果拷贝构造异常,栈的状态会是什么?
top
还没减,栈没变,理论上安全。
- 返回类型是值类型
- 抛出字符串字面量异常不安全
- 抛出
const char*
字符串异常不推荐,缺少类型信息,难以捕获和处理。 - 建议抛出专门的异常类型(如
std::runtime_error
)。
- 抛出
- 异常处理机制缺乏一致性
- 调用者需要捕获这个异常,否则程序崩溃。
- 代码中没有提供栈空时的替代方案,比如返回
std::optional<T>
或者错误码。
- 设计上的挑战
- 这种设计暴露了异常处理和模板泛型代码结合时的复杂性。
- 需要更细致的异常安全设计来避免潜在的资源泄漏或状态不一致。
总结
- 这个
pop()
函数简单易懂,但异常安全和设计有隐患。 - 抛异常是合理的错误处理方式,但异常类型和后续状态管理需要注意。
- 体现了Cargill文章里“异常处理带来的假安全感”,异常并不是万能的。
C++标准库对栈(stack
)接口设计的解决方案,尤其是pop 和 top 的设计区别。我帮你用中文详细解释:
标准库栈(std::stack
)的设计方案
代码示例:
template <class T>
T& stack<T>::top();
template <class T>
void stack<T>::pop();
设计理念解读:
1. top()
返回 元素的引用 (T&
)
top()
只是返回栈顶元素的引用,不做元素删除操作。- 这样避免了返回值拷贝可能带来的异常问题。
- 用户可以通过
top()
获取栈顶元素,然后根据需要操作它。
2. pop()
只做 删除操作,没有返回值
pop()
用来删除栈顶元素,但不返回任何值。- 删除操作通常不会失败(不会抛异常),或者抛异常的情况很少见。
- 这样避免了
pop()
返回对象时拷贝构造可能产生的异常。
为什么这样设计?
- 将“访问”和“修改”操作分开,使异常处理更简单。
- 避免了函数既要返回对象又要修改状态可能引发的异常安全问题。
- 用户先用
top()
获取元素,确保能成功访问后,调用pop()
删除元素。 - 如果栈为空,调用
top()
或pop()
都会抛std::out_of_range
或类似异常,调用者负责捕获。
总结
Cargill栈 pop() 设计 | 标准库栈设计 |
---|---|
pop() 返回值且抛异常 | top() 返回引用,pop() 无返回值 |
可能导致异常安全隐患 | 异常安全更好,职责明确 |
这就是C++标准库对栈操作的推荐设计,提升了代码的健壮性和异常安全。 |
Cargill 的文章《Exception Handling: A False Sense of Security》 所带来的影响。我们来分句理解:
“传播了恐惧、不确定和怀疑”
- Cargill 的文章揭示了异常机制在实际使用中的问题,尤其是异常安全和模板代码结合时的隐患。
- 这让许多开发者开始怀疑:
- 异常机制是否真的“安全”?
- 是否应该在高要求的代码中使用异常?
- 所以文章虽然技术上中肯,但客观上让人更担心异常处理带来的风险。
“有些人说:这证明异常机制并不安全”
-
一些读者或开发者看完文章后得出一个激进结论:
“你看吧!异常处理果然有问题,不能信任!”
-
这种观点忽略了一个事实:
- 问题不是异常机制本身不安全,而是开发者没有正确使用它。
- 就像指针不是坏东西,坏的是不当使用指针。
总结:
这两句话表达的是:
- Cargill 的文章虽然出发点是提醒大家注意异常处理中的陷阱,
- 但实际上传播了某种“对异常机制的不信任”氛围。
- 导致一部分人误以为异常本身就是错误的设计,而不是认识到异常处理需要良好的设计和实践。
Cargill 在他那篇著名文章中的真正结论:
他并没有说“异常机制是不安全的”
- Cargill 的文章指出了异常使用中的潜在风险和误用的后果,
但并不是在否定异常机制本身。 - 他关注的是“异常处理的误用可能带来的假安全感”。
他也没有说“异常太难用了”
- 他不是说“异常机制太复杂”或者“不适合普通程序员”,
而是强调了开发者需要足够理解异常安全原则,不能掉以轻心。 - 换句话说:异常不难用,但容易被滥用或误用。
他确实说了:自己也没有所有的答案
- Cargill 并没有试图提供一种“完美的替代方案”或“万能的异常安全指导”。
- 他只是提出问题,引发思考,让大家意识到:
“我们对异常机制的真正理解还不够深,不能掉以轻心。”
如果你想更深入理解异常安全,我可以继续讲讲 C++ 中的异常安全级别,比如:
- 基本异常安全(Basic Guarantee)
- 强异常安全(Strong Guarantee)
- 不抛异常保证(No-throw Guarantee)
这句话来自 David Abrahams,是对 C++ 中异常处理的一种理性总结。
- “异常处理并不难。”
- “错误处理才是困难的。”
- “而异常机制,让错误处理变得更容易。”
深度理解:
1. “异常处理并不难”
使用
try/catch
、throw
等机制本身其实不复杂。
- C++ 的语法支持异常处理:
try
捕捉,throw
抛出,catch
处理。 - 学习和掌握这套机制不难,写起来也不麻烦。
2. “错误处理才是困难的”
真正难的是你怎么正确、系统地处理程序中可能出现的错误:
- 如何确保资源不会泄漏(如内存、文件句柄)?
- 如何让代码即使中断也保持状态一致?
- 如何确保调用方能正确响应错误?
- 如何写出易于维护和扩展的错误处理逻辑?
这些才是“硬核问题”。
3. “异常机制让错误处理变得更容易”
相比传统方式(如错误码、返回值检查),异常机制更有结构、可维护性更强。
- 异常能自动中断控制流,把错误传播到合适的位置处理。
- 配合 RAII(资源自动释放),能自动清理资源,降低出错概率。
- 写出清晰、模块化的错误处理逻辑,更不容易遗漏。
总结
内容 | 异常处理机制的贡献 |
---|---|
错误本身就很复杂 | 异常提供了清晰的工具去处理它 |
程序流程容易出错 | 异常能自动打断并转移控制流 |
资源管理很难 | 异常结合 RAII 自动管理资源 |
David Abrahams 想表达的核心观点是: |
❝ 异常不是负担,而是我们解决“错误处理”这件难事的有力工具。❞
First Steps
关于如何开始正确地使用异常处理和错误检查机制,是学习异常安全编程的入门建议:
小心地检查返回值或错误码,以便检测和修复问题。
- 在你还没有用异常机制时,很多 API(尤其是 C 风格函数)使用返回值表示错误(如
-1
,NULL
,errno
)。 - 不检查这些返回值,就可能忽略错误,程序会在错误状态下继续执行。
- 所以第一步,是要认真检查每一个可能失败的返回值。
识别可能抛出异常的函数,并思考在失败时该怎么办。 - 当你使用 C++ 中的函数(标准库或你自己的)时,要搞清楚哪些函数可能
throw
。 - 比如:
new
,std::vector::at
,std::string
的构造函数 等等都可能抛出异常。 - 你需要为这些函数设计好失败时的响应策略:是否重试?是否恢复?是否上报?
使用异常规范,让编译器协助生成更安全的代码。 - C++11 之后引入了
noexcept
,意思是“这个函数不会抛异常”。 - 如果一个函数写了
noexcept
却在运行时抛了异常,程序会直接终止(这是故意的:防止意外)。 noexcept
能让编译器更好地优化代码,也能帮助你明确哪些函数必须不抛异常,比如析构函数。
使用 try/catch 块控制程序流程。- 用
try
包裹你认为可能失败的代码,用catch
捕获错误并处理。 - 这样可以把错误处理集中起来,避免在每个函数里都重复写检查逻辑。
- 有助于构建清晰、结构化的错误处理流。
总结:初学者应采取的 4 个步骤
步骤 | 内容 |
---|---|
检查返回值 | 不忽略传统错误码,养成好习惯 |
识别抛异常函数 | 提前思考错误场景,准备应对 |
使用 noexcept | 让代码更安全,编译器更聪明 |
使用 try/catch | 有计划地控制错误流程 |
“The Hard Way”(艰难之路),它列出了在 C++ 中确保程序异常安全时,程序员需要付出的一些“艰难努力”。下面我们逐条翻译并解析它的含义:
认真检查返回值或错误码,以便发现并修复问题。
- 这是传统错误处理方式(如 C 语言)最基础的做法。
- 但它有一个大问题:容易忘记或忽略错误检查,从而导致错误被“悄悄吞掉”,留下隐藏 Bug。
- 所以,这是“艰难之路”的第一步:靠程序员自己时刻小心翼翼地手动检查每个调用的返回值。
识别可能抛出异常的函数,并思考失败时应该如何处理。 - 在 C++ 中,很多库函数都可能
throw
异常,比如分配内存、访问越界等。 - 所以你必须清楚知道:哪些函数会抛异常?失败时你要如何应对?
- 这不是编译器能完全帮你做的,需要程序员在设计时主动考虑,这本身是个“难活”。
使用异常规范(如noexcept
),让编译器帮助你构建更安全的代码。 - 虽然
throw()
异常说明已经被废弃,但noexcept
是现代 C++ 的推荐做法。 - 它告诉编译器:这个函数不会抛异常,有助于优化代码并避免在不可恢复场景下崩溃。
- 但这也意味着你要清楚每个函数的异常行为,不然写错
noexcept
会导致程序崩溃。
使用try/catch
块来控制程序流程。 - 你需要小心安排哪些代码应该放进
try
块,哪些错误需要捕获。 - 如果结构不清晰,
catch
可能会变得复杂,异常可能会被误处理或遗漏。 - 所以这一步也不容易,尤其在大型项目中。
总结
“The Hard Way” 不是说这些做法不好,而是指出:
即使有了异常机制,写出真正安全可靠的代码依然不容易,你仍然需要:
- 细心检查错误
- 明确函数行为
- 设计好错误处理路径
- 正确使用语言特性(
noexcept
、try/catch
)
这些“难点”是 C++ 中构建健壮系统必须面对的。
#“The Wrong Way”(错误的方式)
- 仔细检查返回值 / 错误码,以发现和修复问题
- 识别可能抛出异常的函数,并思考失败时该如何处理
- 使用异常规范,让编译器帮助生成安全代码
- 使用 try/catch 块控制程序流程
1. “仔细检查返回值 / 错误码”
- 表面上没错,但:
- 容易忘记检查
- 不能强制检查(不像异常机制那样必须处理)
- 会让代码冗长、易出错,违背了 C++ 的类型安全初衷
2. “识别可能抛异常的函数”
- 是个好建议,但:
- 没有工具能完全自动化识别,需要程序员自己追踪
- 现代 C++ 更提倡使用 RAII、智能资源管理,减少你“显式识别”的负担
3. “使用异常规范(如 noexcept)”
noexcept
很有用,但:- 滥用
noexcept
会让程序意外终止 - 编写和维护异常规范本身是一件复杂的事,不应该成为初学者的“唯一手段”
4. “使用 try/catch 控制流程”
- 必须掌握,但:
- 不应该“频繁使用 try/catch 控制逻辑流程” —— 这会导致代码分散、逻辑跳跃、难以维护
- 更推荐使用 局部处理、RAII、异常安全容器 代替手动 try/catch
核心观点总结
错误做法的特征 | 正确做法的方向 |
---|---|
全靠程序员手动检查错误 | 用类型系统、RAII 自动化 |
过度依赖 try/catch 捕获异常 | 在设计上避免异常泄露 |
认为写了 catch 就安全了 | 必须考虑异常安全的恢复策略 |
把错误处理放在代码末尾 | 错误处理应融入流程逻辑中 |
最后一句总结:
“The Wrong Way” 是在提醒我们:
不要满足于语法上的 try/catch,而要深入思考程序设计中的异常安全、资源管理和逻辑完整性。
Exception-Safe!
David Abrahams 提出的 C++ 异常安全保证(Exception-Safety Guarantees)三级标准,是 C++ 编程中非常重要的异常处理思想。
异常安全等级(Abrahams)
1. Basic Guarantee(基本保证)
❝ 组件的**不变式(invariants)**得以保持,不会泄漏资源 ❞
- 即使发生异常,程序的内部状态也不会损坏或混乱。
- 比如容器大小、内存指针等依然保持正确状态,不会泄露内存、不导致双重释放。
- 是最低限度的异常安全要求。
示例:
std::vector<int> v;
try {v.push_back(10); // 如果抛异常,v 仍然是有效的 vector(不崩溃、不泄漏)
} catch (...) {// 可以安全处理错误
}
2. Strong Guarantee(强保证)
❝ 如果发生异常,程序状态完全不变(像这次调用根本没发生一样)❞
- 要么操作成功并产生效果,要么失败且程序状态完全不变。
- 比如
std::vector::insert()
会使用临时缓冲区,如果失败,不会修改原容器。 - 非常适合高可靠性需求场景。
示例(假设 copy-swap 实现了强保证):
MyClass a;
MyClass b;
a = b; // 如果 copy 过程中出错,a 不会被部分更新,而是保持原状态
3. No-Throw Guarantee(不抛异常保证)
❝ 操作永远不会抛出异常 ❞
- 例如析构函数、
swap()
、std::vector::clear()
等应提供这一保证。 - 这种级别通常通过
noexcept
声明实现。
示例:
void safe_cleanup() noexcept {// 绝不能抛异常,比如释放内存、关闭文件
}
三种异常安全级别对比表:
级别 | 名称 | 发生异常时系统状态 | 是否资源泄漏 | 应用场景 |
---|---|---|---|---|
3 | No-Throw | 无异常发生 | 无 | 析构函数、关键路径 |
2 | Strong | 状态完全不变 | 无 | 高可靠操作 |
1 | Basic | 状态可能变,但不破坏合法性 | 无 | 大多数 STL 操作 |
0 | Unsafe | 状态混乱,可能资源泄漏 | 有 | 极不推荐 |
总结一句话:
异常安全不是“全部或无”,而是分等级。
你的代码应该明确目标级别,并用合适方式实现它 —— RAII、copy-swap、智能指针都是帮助你达成这些级别的工具。
C++ 异常安全假设(Assumptions)和实践(Guarantees) 的总结,分为调用者视角和实现者视角:
异常安全的假设
- 必须 No-Throw(不能抛异常)的函数:
- 析构函数(destructors)
swap()
- 移动操作(C++11 中的
move constructor
,move assignment
)
深度理解:
这些操作在异常安全编程中必须保证不抛异常,原因如下:
- 析构函数:常用于清理资源(文件、内存等)。如果它抛异常,将在栈展开期间导致程序异常终止(
std::terminate()
)。 - swap():常用于强异常安全实现(例如 copy-and-swap)。若它可能抛异常,将破坏异常处理的基础。
- 移动操作(move):如果对象移动会抛异常,那么 STL 容器等就无法提供高效、强异常安全的操作。
总结:这些函数必须使用noexcept
或实际不抛异常,这已经成为现代 C++ 编程的基本假设。
从调用者角度看异常安全(C++11)
- 我们只要求 “No-Throw Required” 的函数比 Basic 更强(如 swap, 析构)
- 除此之外,假设所有函数都可能抛异常,除非我们明确知道它不会
- 这对某些人来说是意外!
深度理解:
调用者应该持有这样一种态度:
“除了那些我能肯定不会抛异常的函数(如析构、swap),我都假设其他函数是可能失败的。”
这鼓励你在写代码时:
- 不盲信别人的函数不会失败
- 保守处理所有潜在的异常源
- 明确依赖
noexcept
函数构建可靠逻辑
总结:调用者应该对异常更谨慎,除非函数明确声明noexcept
,否则就当它“可能失败”处理。
从实现者角度看异常安全:
- 至少要提供 Basic Guarantee(基本异常安全)
- 对“必须 No-Throw”的函数,务必确保不抛异常
- 如果提供更强的异常保证(如 Strong),请务必写明文档
- 当实现 Strong Guarantee 很“自然”时,就提供它
深度理解:
从实现者(即写库、写类的人)角度看:
目标 | 原因 |
---|---|
Basic Guarantee 是底线 | 防止资源泄漏,保持对象有效 |
No-Throw 要求必须满足 | 否则调用者在异常处理中无法保证安全 |
强异常保证应文档化 | 让调用者放心调用 |
自然就强保障 | 如果实现 Strong Guarantee 不复杂,就应该给出 |
示例: | |
std::vector::push_back() 在某些情况下可以提供强异常安全(先扩容再插入),那实现者应文档说明其行为。 |
总结一句话:
调用者应谨慎,默认所有函数可能抛异常;实现者应至少保证基本安全,力求在关键地方实现强保障并声明。
展示了在 std::vector::push_back
中如何实现异常安全,特别是对 Abrahams 异常安全保证三原则(Basic / Strong / No-Throw) 的应用。
基本结构与情景
template <typename T> struct vector
{...void push_back(T const&); // 向向量添加一个元素...
};
理解:
这里是 vector
的简化定义,强调了我们要分析的是 push_back()
方法的异常安全性。
当 size < capacity
的情况
void push_back(T const& t)
{...new(&buffer[size]) T(t); // 在已有 buffer 中构造新元素++size; // 更新 size...
};
异常安全等级:Strong Guarantee
解析:
- 在已有容量足够的情况下,我们只是在已有内存中原地构造元素。
- 若
T(t)
构造抛异常,++size
不会执行,vector 的状态保持不变。 - 没有内存分配、没有复杂操作,异常前一切都没变,因此自然满足 Strong Guarantee。
Slide 47:当 size == capacity
的情况(需要扩容)
void push_back(T const& t)
{分配一个更大的临时缓冲区;拷贝已有元素到该新缓冲区;在新缓冲区的尾部构造新元素 T(t);交换新旧缓冲区(swap);删除旧缓冲区;++size;
}
异常安全等级:Strong Guarantee
解析:
- 如果任一操作(例如
T(t)
或元素拷贝)抛异常,我们不会替换旧的buffer
,不会修改旧状态。 - 只有 所有步骤成功后,才真正交换并释放旧内存。
- 因此,整个 vector 状态在异常前后保持一致,提供 Strong Guarantee。
对于很多函数来说,只要你实现了 Basic Guarantee,Strong Guarantee 往往可以“顺便”实现。
理解:
- 如果你遵循 RAII、临时变量、swap 等模式,那么要实现 Strong Guarantee 并不困难。
- 例如
std::vector::push_back
就是通过这种方式做到的。 - 所以:不要怕 Strong Guarantee,很多时候你只要稍微努力,就可以实现它。
全文总结:
情况 | 实现 | 异常安全等级 |
---|---|---|
size < capacity | 原地构造并递增 size | Strong |
size == capacity | 分配新缓冲区 + swap | Strong |
主要讲了几个重点:
— 什么时候不提供强异常安全保证(Strong Guarantee),以 vector<>::insert()
为例;
— C++ 异常机制的基本工作原理;
— C++11 中异常检测与抛出的一些细节问题。
什么时候不提供强异常安全保证?
- 以
vector<>::insert()
为例:
要保证强异常安全,需要先复制整个容器,然后在副本中插入元素,完成后再替换原容器。 - 标准库并没有强制要求
insert()
必须提供强异常保证。 - 有些操作实现强保证代价很大,所以标准只要求基本异常安全。
C++ 异常机制的基本工作原理
- 错误检测和抛出(throw):
当程序检测到运行时错误时,可以创建一个错误对象并用throw
抛出。
例子:ObjectType object; throw object;
- 错误处理(catch):
程序其它部分使用try/catch
块捕获并处理异常。
关于抛出对象的一些问题
- 抛出的对象到底是什么?
- 我们可以抛出指针吗?
- 可以抛出引用吗?
这些问题涉及 C++ 异常处理的语法和语义,具体规则是: - 可以抛出任何类型的对象(包括内置类型和自定义类型),这时会发生拷贝构造(或者移动构造)。
- 抛出指针也是允许的,但这一般不推荐,因为捕获异常时需要记住指针指向的对象生命周期。
- 抛出引用是不允许的,因为异常处理机制要求被抛出的对象必须是独立存在的值。
总结
- 并非所有函数都必须实现强异常安全保证,尤其是代价较大的操作。
- C++ 的异常机制通过
throw
抛出对象,catch
捕获对象来实现错误传播。 - 理解抛出对象的类型和方式,对写好异常安全代码很重要。
C++异常机制的工作流程和相关细节,重点在错误检测、抛出、捕获以及C++11的改进。以下是翻译和理解:
C++异常机制概述
- 错误检测 / 抛出(throw):检测到错误时,通过
throw
抛出异常对象。 - 错误处理 / 捕获(catch):通过
try/catch
块捕获异常并处理。 - C++11新增特性。
代码示例与说明
try {code_that_might_throw(); // 可能抛出异常的代码
} catch (A a) { // 捕获异常对象,像函数参数一样接收error_handling_code_that_can_use_a(a);
} catch (...) { // 捕获所有异常的“万能”捕获器more_generic_error_handling_code();
}
more_code(); // 异常处理后继续执行的代码
捕获异常的注意点
- 按值捕获(catch by value)的问题:
- 可能发生对象切片(slicing),导致丢失派生类信息。
- 捕获时会拷贝异常对象,拷贝过程本身可能抛异常。
- 推荐按引用捕获(catch by reference):
catch (A& a) {a.mutating_member();throw; // 重新抛出当前异常 }
- 这样避免了切片和多余拷贝。
捕获继承关系的异常示例
假设 B
是 A
的基类,捕获顺序和方式有多种:
try {throw A();
} catch (B) { } // 捕获B类型(按值)catch (B&) { } // 按引用捕获Bcatch (B const&) { }catch (A) { } // 捕获Acatch (A&) { }catch (void*) { } // 捕获指针异常catch (...) { } // 捕获所有异常
重要原则总结
- 异常抛出时应“按值抛出”(throw by value),即抛出异常对象的副本。
- 异常捕获时应“按引用捕获”(catch by reference),避免切片和额外的拷贝。
C++ 中 try/catch
的性能开销,以及特殊的“函数 try 块”(Function Try Blocks)用法,特别是在构造函数中的应用。
try/catch
的性能开销
- 不抛异常时,几乎没有性能开销。
- 抛异常时,性能开销不确定,且不容易测量,也不太关心。
也就是说,正常代码路径下,try
块的存在不会降低性能;只有异常真的被抛出时,才会产生额外成本。
函数中的 try/catch
结构示例
void F(int a) {try {int b;// 可能抛出异常的代码}catch (std::exception const& ex) {// 可以访问 a 但不能访问 b// 可以抛出新异常,返回或结束函数}
}
- 注意:
catch
块中只能访问函数参数a
,不能访问try
块内定义的局部变量b
,因为它们的作用域在异常发生时已经结束。
函数 try 块(Function Try Blocks)
- 函数 try 块 是一种特殊的
try/catch
用法,可以包裹整个函数体,尤其常用于构造函数。 - 它的语法:
void F(int a) try {int b;// 函数体
} catch (std::exception const& ex) {// 只能访问参数 a,不能访问函数内部变量(比如 b)// 可以处理异常,修改异常对象或抛出不同异常
}
函数 try 块在构造函数中的作用
- 构造函数在初始化列表中可能会抛异常(基类构造函数或成员构造函数抛出异常)。
- 普通
try/catch
无法捕获初始化列表中的异常,函数 try 块能捕获这些异常。
示例:
Foo::Foo(int a) try : Base(a), member(a) {// 构造函数主体
} catch (std::exception& ex) {// 只能访问参数 a,不能访问 Base 或 member 对象// 可以修改异常或抛出新异常// 不能返回,构造函数必须抛出异常结束
}
总结
try/catch
不影响正常执行路径性能,只在抛异常时有开销。- 函数 try 块适合捕获构造函数初始化列表中的异常。
- 函数 try 块的
catch
中只能访问函数参数,不能访问局部变量或已构造的成员。 - 构造函数的异常处理只能抛出异常,不能用
return
结束。
函数 try 块的唯一用途
- 函数 try 块主要用来修改由基类构造函数或成员构造函数抛出的异常。
- 换句话说,当基类或成员的构造抛出异常时,函数 try 块可以捕获该异常,并对其进行修改或者重新抛出不同的异常。
例外情况
- 除了这个典型用途外,还有一些特殊用法,比如在网站
exceptionsafecode.com
上的esc.hpp
中有提到的其他用法。
总结:
函数 try 块主要是为构造函数初始化列表中抛出的异常提供“最后一道防线”,帮助开发者控制和修改异常行为。
C++11 新增的异常处理功能,尤其是在多线程间传递异常的支持。:
C++11 支持的异常处理场景
- 支持在线程之间传递异常。
- 支持异常的嵌套。
线程间移动异常
- 捕获异常(capture the exception)很简单。
- 异常对象可以像普通对象一样被移动(move)。
- 可以在需要的时候重新抛出异常(re-throw)。
具体机制
<exception>
头文件中定义了:
std::exception_ptr current_exception() noexcept;
std::exception_ptr
是一种智能指针,专门用于持有异常对象的指针。
std::exception_ptr
的特点
- 可拷贝(copyable)。
- 异常对象会一直存在,只要有任意
std::exception_ptr
持有它。 - 可以像普通数据一样在线程间传递和拷贝。
总结
C++11 通过 std::exception_ptr
提供了安全且方便的机制,让异常可以跨线程捕获、存储、传递和重新抛出,极大地提升了多线程异常处理的能力和灵活性。
C++11 中如何在线程间捕获、保存和重新抛出异常,具体内容和流程如下:
线程间移动异常示例流程
std::exception_ptr ex(nullptr);
try {// 可能抛出异常的代码
}
catch(...) {ex = std::current_exception(); // 捕获当前异常并存储到 ex 中// 这里可以做一些处理
}
if (ex) {// ex 非空,表示捕获了异常// 可以在适当的时候重新抛出异常
}
重新抛出异常
<exception>
头文件中定义了函数:
[[noreturn]] void rethrow_exception(std::exception_ptr p);
- 通过
rethrow_exception(ex)
可以重新抛出之前捕获的异常。
相关场景示例
int Func(); // 可能抛出异常的函数
std::future<int> f = std::async(Func); // 异步调用
int v = f.get(); // 如果 Func() 抛了异常,这里会重新抛出该异常
- 使用
std::async
启动异步任务时,异常会被捕获并保存在std::future
中。调用get()
时,异常会被重新抛出。
总结
- 使用
std::exception_ptr
方便在线程间保存异常。 - 可以在合适的时机重新抛出异常。
std::async
与std::future
自带异常传播机制,异步函数抛的异常会在get()
时重新抛出。
C++11 中“嵌套异常”(Nesting Exceptions)的机制,允许在捕获异常的基础上再抛出新的异常,同时保留原始异常信息,便于后续处理。
嵌套异常概念
- C++11 支持将当前异常“嵌套”到一个新的异常中。
- 你可以抛出一个新的异常,同时携带之前捕获的异常。
- 也可以单独重新抛出嵌套的那个原始异常。
具体机制
<exception>
头文件中定义了:
class nested_exception;
- 这个类的构造函数会隐式调用
current_exception()
,并保存当前异常对象。
抛出带嵌套异常的新异常
<exception>
中还定义了模板函数:
[[noreturn]] template <class T>
void throw_with_nested(T&& t);
- 它会抛出一个类型继承自
T
和std::nested_exception
的对象。
例子示范
try {try {// 可能抛出异常的代码} catch(...) {std::throw_with_nested(MyException()); // 抛出带嵌套异常的新异常}
} catch (MyException& ex) {// 处理新异常 ex// 检查 ex 是否包含嵌套异常// 提取并重新抛出嵌套的异常
}
重新抛出嵌套异常
<exception>
中还提供了:
template <class E>
void rethrow_if_nested(E const& e);
- 只需调用此函数即可检查并重新抛出嵌套异常。
总结
- C++11 通过嵌套异常机制,允许在处理异常时保留并传递原始异常信息。
- 这样在复杂系统中调试和定位问题时,更加方便和可靠。
这段内容是对 C++11 嵌套异常机制的具体示例,重点在于如何捕获异常、包装成带嵌套信息的新异常,并且如何在外层捕获时重新抛出嵌套的原始异常。
嵌套异常示例代码解读
try {try {// 可能抛出异常的代码} catch(...) {std::throw_with_nested(MyException()); // 抛出一个带有当前异常的嵌套异常}
} catch (MyException& ex) {// 处理 MyException 类型异常// 重新抛出嵌套的异常(如果有)std::rethrow_if_nested(ex);
}
关键点
- 内层
try
捕获所有异常(catch(...)
),并用std::throw_with_nested
抛出一个新的异常MyException
,同时保存了之前捕获的异常。 - 外层
try
捕获到MyException
后,使用std::rethrow_if_nested
来检测并重新抛出内部保存的嵌套异常。 - 这样可以在异常处理链中保持异常信息的完整性,方便调试和错误追踪。
总结
std::throw_with_nested
用于抛出带有嵌套异常的新异常。std::rethrow_if_nested
用于在捕获异常时重新抛出内层的嵌套异常。- 通过这种方式,异常的层级关系不会丢失,能够更细致地了解异常的传播过程。
C++ 标准中的两种异常处理相关的“处理程序”(Handlers),以及它们的特点和限制。下面是翻译和理解:
标准异常处理程序
1. “Terminate” 处理程序
- 默认行为是调用
std::abort()
,直接终止程序。 - 我们可以自定义自己的 terminate 处理程序。
- 但是,调用时已经“太晚”了(意味着程序处于不可恢复状态,无法继续正常运行)。
2. “Unexpected” 处理程序
- 当程序抛出了一个不符合动态异常规范(dynamic exception specifications)的异常时,会调用 unexpected 处理程序。
- 默认行为是调用 terminate 处理程序(也就是直接终止程序)。
- 同样,我们可以自定义自己的 unexpected 处理程序。
- 但同样是“太晚”了,无法恢复正常流程。
总结
- 这两种处理程序都是在程序发生严重异常时的“最后一道防线”。
- 它们的调用意味着程序无法正常继续执行,只有终止退出的选择。
- 尽管可以自定义这些处理程序,但它们的时机已经是“异常发生后”的最终状态,难以实现真正的错误恢复。
动态异常规范(Dynamic Exception Specifications) 在 C++ 中的用法和特点:
动态异常规范
void F();
—— 可以抛出任何异常。void G() throw (A, B);
—— 只能抛出类型为A
或B
的异常。void H() throw ();
—— 不允许抛出任何异常。
重要特点
- 动态异常规范不会在编译时进行检查,也就是说编译器不会在编译阶段报错。
- 异常规范是在运行时强制执行的:
如果函数抛出了不在异常规范中的异常,程序会调用“unexpected”处理程序,默认情况下该处理程序会终止程序。
小结
动态异常规范是 C++早期提供的异常控制机制,虽然有一定的约束作用,但由于仅在运行时检测且容易引起程序终止,现代 C++(自 C++11 起)逐渐弃用了这一机制,推荐使用 noexcept
来声明不会抛出异常的函数。
这段内容介绍了 C++11 中 noexcept
关键字的两种用法,以及它的行为:
noexcept
的两种用法
1. 作为函数的异常规范(noexcept specification)
void F();
—— 可能抛出任何异常。void G() noexcept(Boolean constexpr);
——G
函数声明时,使用noexcept
来指定是否不抛异常,参数是一个编译时常量表达式(true
或false
)。void G() noexcept;
—— 默认等价于noexcept(true)
,表示函数不会抛异常。- 析构函数默认是
noexcept(true)
,即默认不抛异常。
2. 作为操作符(noexcept operator)
- 语法:
noexcept(expression)
用来在编译时检查某个表达式是否会抛异常,返回一个布尔常量。 - 示例:
static_assert(noexcept(2 + 3), ""); // 通过,2+3 不会抛异常 static_assert(!noexcept(throw 23), ""); // 通过,throw 23 会抛异常 inline int Foo() { return 0; } static_assert(noexcept(Foo()), ""); // 失败,Foo() 也被视为可能抛异常
- 注意:
noexcept(Foo())
这个断言失败,说明编译器无法确定Foo()
是否抛异常,默认保守地认为可能抛异常。
总结
noexcept
关键字既可用于声明函数是否保证不抛异常,也可以用于编译时检测表达式是否会抛异常。- 析构函数默认是不抛异常的(
noexcept(true)
)。 - 使用
noexcept
操作符时,编译器会对表达式进行异常安全性分析,但有时结果可能不如预期(例如普通函数调用被视为可能抛异常)。
noexcept
在实际代码中的使用方式,重点是它的三种用法场景:
noexcept
的使用方式
- 作为操作符
用于基于是否不抛异常来进行优化:
例如,在移动(move)操作时,如果移动构造函数保证不抛异常,则使用移动;否则退而求其次,使用更昂贵的复制操作。 - 无条件形式
适用于简单的用户自定义类型,直接声明构造函数不会抛异常:struct Foo { Foo() noexcept {} // 明确表示构造函数不会抛异常 };
- 条件形式(结合模板和操作符)
用于模板中根据模板参数的异常规范决定自身的异常规范:template <typename T> struct Foo : T {Foo() noexcept(noexcept(T())) {} // 只有当 T() 构造函数不抛异常时,Foo() 才保证不抛异常 };
总结
noexcept
操作符和异常规范结合起来,可以实现更细粒度的异常安全控制,特别是模板中自动推断异常特性。- 这让代码更安全,也能提高效率,比如更好地利用移动语义。
不要使用动态异常说明符(dynamic exception specifications)!
应该使用 noexcept
来替代。
标准处理程序 — “终止(Terminate)”处理程序
- 当异常未被捕获时调用。
- 当重新抛出异常但当前没有异常时调用。
- 当调用
std::rethrow_exception()
传入空指针时调用。 - 当
noexcept
标记的函数抛出异常时调用。 - 当在已经有异常正在传播时再抛出异常时调用。
这就是说,terminate
是程序异常处理链条中的最后防线,一旦调用它,程序通常会直接终止。
如果你需要,可以自定义terminate
处理函数,但注意此时通常为“太迟了”,无法安全恢复。
析构函数绝对不能抛出异常。
- 它必须保证 无异常(No-Throw Guarantee)。
- 清理资源的操作必须始终安全可靠。
- 虽然析构函数内部可以处理异常(例如捕获并处理),但绝对不能让异常“传出去”。
对象生命周期规则:
- 构造顺序:
- 基类对象先构造
- 成员变量按类型定义中声明的顺序(从左到右或从上到下)构造,不是按照构造函数初始化列表中的顺序
- 最后执行构造函数体的代码
- 析构顺序:
按构造顺序的严格逆序析构 - 对象生命周期何时开始?
对象的生命周期从其构造函数执行完毕开始,资源被正确初始化后,生命周期正式开始。
总结:构造顺序固定,成员变量构造顺序与初始化列表无关,析构则严格反向。
构造(构造异常)发生时的处理:
- 异常发生在:
- 基类构造函数
- 成员对象构造函数
- 构造函数体内
- 需要清理的内容:
- 已成功构造的基类对象会自动析构
- 已成功构造的成员对象会自动析构
- 构造函数体内自己手动分配的资源(如
new
出来的内存)必须手动清理,因为此时对象析构函数不会被调用 - 对象本身的内存(通常由编译器或分配器管理,构造失败时会释放)
总结:构造异常时,已构造部分会自动清理,未构造的部分不会调用析构,构造函数体内分配的资源需手动释放。
构造函数中抛出异常导致构造中止时,需注意:
- 对象内存泄漏风险
如果构造函数内使用了动态分配(如new
),且异常导致构造失败,这部分内存可能不会被自动释放,造成内存泄漏。 - Placement new 的处理
使用 placement new(在已分配的内存上构造对象)时,如果构造函数抛异常,必须确保该内存被正确释放或回收,因为析构函数不会被调用。
Placement New 指的是使用 new
操作符时,传入额外参数来指定对象的构造位置:
- 标准中提供了“原始的 placement new”版本,允许在指定的内存地址上构造对象。
- 例如:
这条语句表示在Object* obj = new(&buffer) Object;
buffer
指向的内存位置构造一个Object
对象。 - 这里的 “placement” 可能会让人误解,以为是“放置”,实际上是“在指定地址上构造”。
总结:Placement new 是一种在已有内存上构造对象的技术,不会分配新内存,只调用构造函数。
中止构造(Aborted Construction)
当构造函数抛出异常时可能导致的问题:
- 对象内存泄漏:如果构造函数抛异常,分配的内存可能无法释放,导致内存泄漏。
- Placement new:使用 placement new 时,更要注意异常处理。
Effective C++ 第3版,条目52 说: - 如果你写了 placement new,就应该写对应的 placement delete。
Placement Delete: - 由于 delete 操作符无法接收参数,
- placement delete 只有在对应的 placement new 调用时构造函数抛异常才会被调用,
- 如果没有定义 placement delete,不会报错(即不是强制的)。
总结:为了避免在使用 placement new 时因构造函数异常导致内存泄漏,最好提供对应的 placement delete 实现。
RAII 示例:
大多数智能指针都是 RAII 的典型应用!
还有很多资源的包装类,比如:
- 内存管理
- 文件操作
- 互斥锁(mutex)
- 网络套接字
- 图形端口
这些资源的获取与释放,都通过对象的构造和析构自动管理,保证异常安全和资源不泄漏。
对象根本就不存在!
- 如果你有这个对象,说明你已经成功获取了资源。
- 如果获取资源失败了,那么构造函数会抛出异常,这时对象根本没有被创建。
也就是说,资源获取失败时,对象不会存在,不需要担心对象的清理问题。
RAII 清理
- 析构函数负责释放资源。
- 有些对象可能有一个“release”成员函数用来主动释放资源。
- 清理过程不能抛出异常!
- 析构函数绝对不能抛出异常。
设计指导原则
- 每个项目(函数或类型)只做一件事。
- 不应有对象管理多个资源。
- 每个资源都应该封装在对象中。
- 如果资源不在对象里,析构时无法自动清理,可能导致资源泄漏。
- 智能指针是管理资源的好帮手。
智能指针(特别是 shared_ptr
)的使用规则、潜在陷阱以及更安全的用法。以下是内容的逐句翻译和理解:
shared_ptr
- 智能指针(smart pointer)的一种。
- 最初来自 Boost 库。
- 后来被纳入 TR1(技术报告1)。
- 最终成为 C++11 标准的一部分。
- 使用引用计数(ref-counted)来管理资源。
- 支持自定义删除器(custom deleter)。
智能指针的陷阱(Gotcha)
FooBar(smart_ptr<Foo>(new Foo(f)),smart_ptr<Bar>(new Bar(b)));
这个写法安全吗?
答:不安全,原因是:
- 如果
new Foo(f)
成功,但new Bar(b)
抛出异常,那么Foo
的指针还没有交给智能指针管理,可能会导致内存泄漏。 - 也就是说,在表达式中同时包含多个可能抛异常的操作(如多个
new
)是不安全的。
经验规则(Rule)
- “No more than one new in any statement.”
- 每个语句里不要包含多个
new
,因为多个分配操作意味着多个责任,在中途出错时很难处理。
- 每个语句里不要包含多个
- “Never incur a responsibility as part of an expression that can throw.”
- 不要在可能抛异常的表达式中承担资源管理责任(如
new
)。
例子:
- 不要在可能抛异常的表达式中承担资源管理责任(如
smart_ptr<T> t(new T);
虽然这里有分配和构造操作,但这两者不会同时进行,因此是安全的。
更复杂的例子
smart_ptr<Foo> t(new Foo(F()));
这个安全吗?
是安全的,因为:
F()
是在传递给new Foo()
之前就已经求值完成的。- 构造完成后立刻将指针交给
smart_ptr
管理,不会中间断掉。
Dimov’s Rule(迪莫夫规则)
“为每一个资源分配后立即把它的所有权交给一个唯一的资源管理对象,并且这个管理对象不应该同时管理其它资源。”
换句话说:一个资源、一个管理者、立即交接。
更好的做法(推荐)
使用工厂函数 std::make_shared
或 std::make_unique
:
auto r = std::make_shared<Foo>(f);
auto s = std::make_unique<Foo>(f);
优点:
- 更高效(更少的内存分配次数)。
- 更安全(不会中途出现未托管资源)。
那这个呢?
FooBar(std::make_shared<Foo>(f),std::make_shared<Bar>(b));
这是安全的。 因为:
make_shared
是单个原子操作,构造和资源分配同时完成,异常安全性好。- 如果
make_shared<Foo>
成功但make_shared<Bar>
失败,Foo
的资源会自动被释放。
总结
shared_ptr
是强大的工具,但要用得安全。- 遵循“一次只托管一个资源,并立刻交接”的原则。
- 尽量使用
make_shared
和make_unique
避免手动new
。 - 表达式中避免混合多个可能抛异常的操作。
关于如何用 RAII(Resource Acquisition Is Initialization,资源获取即初始化)原则管理“状态”,也就是把状态当成资源来管理。以下是逐句解释和理解:
Manage State Like a Resource
像管理资源一样管理状态。
- 我们通常使用对象来管理资源,比如内存、文件、锁等。
- 同样地,也应该使用对象来管理程序中的“状态”,比如配置状态、事务状态、标志位、异常屏蔽状态等。
Resource Acquisition Is Initialization(RAII)
这是 C++ 中非常核心的一种编程技术。含义是:
当对象被创建(初始化)时就“获取资源”;当对象销毁时,自动“释放资源”。
比如:
std::lock_guard<std::mutex> lock(m); // 自动加锁,作用域结束自动释放
“Resource” includes too much / too little
“资源”这个词本身有点模糊,可能包含得太多或太少。
- “资源”在 RAII 中的含义本来是广义的:不仅是内存、文件,也包括锁、网络连接,甚至是程序状态。
- 所以,有时说“资源获取即初始化”会让人误解,只想到“物理资源”。
所以建议换个说法:
Responsibility Acquisition Is Initialization(责任获取即初始化)
- 每个对象负责管理某个“责任”或“状态”。
- 一旦对象构造成功,就代表已经承担了某项责任(比如加锁、设置标志、打开连接)。
- 对象析构时必须负责释放这个责任(如解锁、恢复状态、关闭连接)。
Responsibility Leaks(责任泄漏)
- 就像内存泄漏一样,如果没有合适的机制释放责任,也会发生“责任泄漏”。
- 比如:设置了某个全局状态但忘记恢复,就可能影响其他逻辑。
Responsibility Management(责任管理)
- 正确的做法是用一个类来“封装责任”的申请和释放。
- 构造时承担责任,析构时释放责任 → 安全、高效、不容易出错。
示例:
class FlagSetter {bool& flag;
public:FlagSetter(bool& f) : flag(f) { flag = true; }~FlagSetter() { flag = false; }
};
// 用法
{FlagSetter temp(state.isBusy); // 设置 isBusy = true// 做事...
} // 自动还原 isBusy = false
总结
要点如下:
- RAII 不只是用于资源(如内存、文件),也可以用于管理“状态”或“责任”。
- 推荐从 “责任管理” 的角度理解 RAII。
- 使用类对象来承担状态管理,可以防止忘记恢复状态、泄漏逻辑责任。
- 这能让代码更清晰、更安全,尤其在异常或复杂控制流场景下。
如需,我可以为你写几个实际例子帮助更好理解这个理念。
C++ 中 异常安全(exception safety)设计原则、RAII 清理机制、以及如何使用 swap()
(Swapperator)实现强异常安全保证。下面我为你梳理并解释每个核心概念和例子。
清理责任:RAII 和异常安全
原始代码示例(危险):
dosomething();
cleanup(); // 如果 dosomething() 抛异常,这一行不会执行
更安全的做法:
{CleanupType cleanup; // 构造时就注册清理行为,作用域结束自动调用析构函数dosomething(); // 如果抛异常,cleanup 的析构仍然会执行
}
- Joel Spolsky 说:“异常非常危险。”
- Jon Kalb 说:“异常安全的代码是极度安全的。”
- 关键点:所有清理动作必须写在析构函数中,并用对象来管理它。
Cargill Widget 示例(异常安全赋值)
这个例子讨论了一个类 Widget
的赋值操作符如何处理异常安全问题。
原始设计(有缺陷):
Widget& Widget::operator=(Widget const& rhs) {T1 original(t1_); // 保存旧的 t1_t1_ = rhs.t1_;try {t2_ = rhs.t2_; // 可能抛异常} catch (...) {t1_ = original; // 恢复 t1_,但这个恢复也可能抛异常!throw;}
}
- 如果
t1_ = original;
再次抛异常,就会终止程序(std::terminate)。 - 所以,这样并不能提供“强保证”(Strong Guarantee)。
Cargill 的观点:
- 异常安全不能后期补丁,必须在类设计之初就考虑进去。
- 最终结论:“不能(做到强保证)。”(No, it can’t be done)
Jon 的观点(反驳):
- “可以的!Yes, it can.”
- 使用更优设计,比如:copy-and-swap idiom(见下文)
The Swapperator —— swap()
是实现异常安全的关键
为什么使用 swap()
?
- swap 可以做到不抛异常(如果你自定义的
swap()
是 noexcept 的话) - 比如赋值操作可以这样写:
Widget temp(rhs); // 复制 rhs,如果失败不会改动当前对象
swap(temp); // 无异常地交换内容(最好是 noexcept)
return *this;
自定义 swap()
的规范写法
1. 成员函数版本(类内):
struct BigInt {void swap(BigInt& other) noexcept {std::swap(this->data, other.data);}
};
2. 自由函数版本(类外):
namespace std {template<>void swap<BigInt>(BigInt& a, BigInt& b) {a.swap(b);}
}
注意:
- 模板类不要放到
std
命名空间,普通类可以特化。 swap()
不能抛异常,否则达不到目的。- 不要在类中使用
const
或reference
类型数据成员,因为它们不能被 swap!
为什么不使用 std::swap 默认版本?
std::swap<T>()
默认实现是 3 次 copy:
T temp = a;
a = b;
b = temp;
- 复制可能抛异常,所以不是 noexcept!
Guideline 总结:
好做法
- 使用对象负责清理责任(RAII)
- 清理操作写进析构函数
- 使用自定义的
swap()
实现强异常安全 - 用
copy-and-swap
模式实现赋值操作
避免
- 在表达式中隐含多个“责任”(比如多个 new)
- 在可以抛异常的代码中延迟清理动作
- 使用 const/references 成员导致类型不 swappable
C++11 中“Swapperator”的改进与异常安全性的验证机制。以下是你需要理解的要点解释和深度解析:
什么是 “Swapperator”?
“Swapperator” 是一种编程习惯/技术,核心是自定义 swap()
函数来支持:
- 无异常抛出(
noexcept
)的交换操作 - 高效的移动操作(利用 C++11 的 move 语义)
- 强异常安全保证(strong exception safety)
C++11 的改进:std::swap()
使用 move
template<typename T>
void swap(T& a, T& b) noexcept( /* 条件 */ ) {T temp = std::move(a);a = std::move(b);b = std::move(temp);
}
优势:
- 更快:使用
move
而不是copy
- 更安全:如果
move
是noexcept
,整个swap()
也可以是noexcept
新规则:The Rule of 5
- 如果你自定义了 拷贝构造、拷贝赋值,你也应该显式自定义:
- 移动构造
- 移动赋值
- 析构函数
否则:编译器不会自动生成正确的 move 操作,性能或安全可能出问题。
如何验证 Swapperator 是否 noexcept?
Jon Kalb 和 esc
库 提供了一个实用工具:
check_swap()
template <typename T>
void check_swap(T* const t = 0) {static_assert(noexcept(delete t), "delete must be noexcept");static_assert(noexcept(T(std::move(*t))), "move ctor must be noexcept");static_assert(noexcept(*t = std::move(*t)), "move assign must be noexcept");using std::swap;static_assert(noexcept(swap(*t, *t)), "swap must be noexcept");
}
它检查:
- 删除对象是否不会抛异常
- 移动构造是否不会抛异常
- 移动赋值是否不会抛异常
swap()
是否不会抛异常
示例:验证标准类型
std::string a;
esc::check_swap(&a); // 如果 string 的 move 是 noexcept,这个就通过
esc::check_swap<std::vector<int>>(); // vector 的 move 检查
实际应用中,哪里用 check_swap?
struct MyType {void someMethod() {esc::check_swap(this); // 在方法中验证当前类型的 Swapperator 是否 noexcept...}
};
这可用作模板类中对异常安全性的静态检查工具。
延伸理解:为何一定要 noexcept?
- STL 容器和算法对
swap()
是否 noexcept 是有依赖的:- 如果你自定义了
swap()
,而它不是 noexcept,那么:std::sort()
会选择 复制而非交换- 容器的
resize()
、insert()
可能触发 不必要的复制
- 如果你自定义了
总结:Swapperator 使用指南(C++11+)
项目 | 建议做法 |
---|---|
定义 swap() | 自定义为 noexcept 的成员函数 + 非成员函数 |
使用移动操作 | std::move() 实现移动构造与赋值 |
使用 check_swap() | 编译时验证你的类型的 Swapperator 是否 noexcept |
避免 const 成员 | const 成员不能 swap,破坏 noexcept 保证 |
避免引用成员 | 引用成员也不能 swap |
如果你希望,我可以为你写一个完整的类,展示怎样为自定义类型实现: |
move
构造/赋值noexcept
的swap()
check_swap()
的使用
C++ 中的 异常安全性设计理念,特别是 swap
和 noexcept
的使用规则。以下是核心概念的分解与详细解释:
Calling swap()
in Templates
推荐调用方式:
template <typename T>
void someFunction(T& a, T& b) {using std::swap; // 启用 ADL(Argument-Dependent Lookup)swap(a, b); // 优先使用用户定义的 swap,而不是 std::swap<T>
}
原因:
std::swap
是默认实现(使用 copy)- 自定义类型若定义了更高效的
swap()
,你希望通过 ADL 自动找到它 - 这样比直接写
std::swap(a, b);
更泛化、更高效
替代方案:Boost 的 boost::swap()
#include <boost/swap.hpp>
boost::swap(a, b);
- Boost 提供了一个更通用、兼容 ADL 的 swap 实现
- 有些旧项目可能会使用它替代标准方式
创建 Swapperator 的指导原则
Value 类必须:
- 提供
swap()
成员函数或非成员函数 - 保证 No-Throw 安全性
struct Widget {void swap(Widget& other) noexcept {using std::swap;swap(x, other.x);swap(y, other.y);}
};
namespace std {template <>void swap(Widget& a, Widget& b) noexcept {a.swap(b);}
}
不再推荐的做法:Dynamic Exception Specifications
void foo() throw(); // 已废弃,C++17 起删除
void bar() throw(int); // 不再被支持
推荐的做法:使用 noexcept
void foo() noexcept; // 不会抛异常
void bar() noexcept(false); // 明确说明会抛异常
void baz() noexcept(noexcept(op)); // 条件 noexcept
在哪里使用 noexcept
?
场景 | 推荐使用 noexcept ? |
---|---|
析构函数(destructor) | 默认就是 noexcept |
swap() 函数 | 应该是 noexcept |
移动构造/赋值(move) | 应该是 noexcept |
其他“资源释放”相关函数 | 如果能保证安全 |
不确定是否能抛异常的代码 | 不要写 noexcept |
指导方针总结
Guideline 编号 | 内容 |
---|---|
151–154 | 所有值类型都应提供 No-Throw 的 Swapperator |
155-1~155-4 | 永远不要使用 dynamic exception specs,改用 noexcept |
156 | 除非明确知道可能抛异常,否则一切资源清理函数都应标为 noexcept |
理解总结
核心目标:
构建 异常安全 的代码,让资源管理更加可靠和高效。
swap 的意义:
- 提供一种 安全地替换对象状态 的方法
- 结合
noexcept
保证在异常发生时不会导致 undefined behavior - 与 STL/模板机制无缝集成(通过 ADL)
如果你需要,我可以为你提供一份完整的示例代码,展示如何: - 为一个值类型设计高效的、noexcept 的
swap
- 编写模板函数安全地调用
swap
- 使用
check_swap()
编译时验证异常安全性
主要围绕 异常安全(Exception Safety) 中的 Strong Guarantee 设计理念,特别强调所谓的 Critical Line ——即 不能失败的那一段代码。
下面是完整的解释与理解要点:
核心概念一:什么是 Critical Line?
Critical Line(关键线) 是代码中那一部分 必须保证不抛异常的操作,因为一旦这部分失败,程序可能会留下“不完整状态”或资源泄露。
Strong Guarantee(强保证):
- 如果操作失败,程序状态保持不变。
- 通常通过先操作临时副本 → 成功后再一次性替换原始状态。
从错误到改进的演进(assignment operator)
你看到的 ResourceOwner
示例展示了从不安全到强异常保证的演变过程。
1️ 初始实现(不安全):
ResourceOwner& operator=(const ResourceOwner& rhs) {delete mResource;mResource = new Resource(*rhs.mResource); // 可能抛异常return *this;
}
- 如果
new
抛异常,你的资源已经被 delete,留下 dangling 指针或资源泄露。
2️ 改进:检查 self-assignment
if (this != &rhs) {delete mResource;mResource = new Resource(*rhs.mResource);
}
- 防止自赋值
- 仍然存在异常风险
3️ 更安全:先复制临时副本
Resource temp(*rhs.mResource); // 如果抛异常,原对象未变
temp.swap(*mResource); // noexcept 的关键线
- 强保证:若构造临时对象失败,不影响原对象
- swap() 是 noexcept 的 critical line,确保最终替换原对象不会失败
最安全写法(C++11 风格,使用 copy-and-swap idiom):
void swap(ResourceOwner&) noexcept;
ResourceOwner& operator=(ResourceOwner rhs) {swap(rhs); // noexceptreturn *this;
}
- 参数传值完成了副本构造
- swap 保证 noexcept
- 极其简洁、安全、高效
C++11 的改进:Rule of 5
在 C++11 中你还应该补充:
ResourceOwner& operator=(ResourceOwner&& rhs) noexcept;
- 结合移动语义提供更高效的赋值路径
总结:如何使用 Critical Line 实现 Strong Guarantee?
步骤 | 描述 |
---|---|
1 | 先构造临时副本(可能失败,但原对象未受影响) |
2 | 对原对象执行 swap() 或 move(noexcept,Critical Line) |
3 | 返回当前对象 |
ResourceOwner& operator=(ResourceOwner const& rhs) {ResourceOwner temp(rhs); // 可能失败swap(temp); // noexcept critical linereturn *this;
}
实用指导语(Guidelines)
- Use Critical Lines to isolate risky code from guaranteed-safe code.
- Copy-then-swap 是实现强异常安全的经典模式。
- 所有
swap()
应 noexcept,这是保证异常安全的核心要求。 - 使用 C++11 的 移动语义 实现更高效、异常安全的资源管理。
- 永远不要在资源操作中混合“可能失败”和“状态修改”操作。
如果你想要,我可以为你写一个完整例子代码,涵盖: - 构造函数/析构函数
- 拷贝/移动构造 & 赋值
swap()
的 noexcept 实现- 如何在使用中保证强异常安全(Strong Guarantee)
C++ 编程中关于异常安全、资源管理、接口设计的最佳实践,常被称为 Cargill Widget Example 背后的哲学。下面我将帮你逐步归纳与理解各部分内容:
一、The Cargill Widget Example
这是一个经典例子,用来说明如何通过 copy-and-swap idiom 实现 强异常安全保证(Strong Guarantee)。
示例回顾:
Widget& Widget::operator=(Widget const& rhs) {T1 tempT1(rhs.t1_);T2 tempT2(rhs.t2_);// The Critical Linet1_.swap(tempT1);t2_.swap(tempT2);return *this;
}
###关键理解:
- 复制副本先构造:可能失败 → 但不会影响当前对象
- swap 替换状态:不抛异常(Critical Line)
- 返回当前对象:完成赋值操作
结果:
提供 强异常安全
高效 & 正确
简洁、可维护
二、Where to try/catch = 三种情况
C++ 不鼓励泛滥使用 try/catch
,但以下三种情况是推荐使用的:
1. Switch(切换错误处理机制)
- 改变错误报告方式(如返回 error code 或记录日志)
- 通常用于低级 API 接口(C-API、系统调用)
2. Strategy(替代方案)
- 有可用的“备选方案”
- 例如:某功能失败 → 降级处理、加载默认配置等
3. Some Success(部分成功)
- 即便失败,系统仍可以“部分成功”继续运行
- 常见于:UI、网络通信、插件机制
三、Scott Meyers 最重要设计原则
“Make interfaces easy to use correctly and hard to use incorrectly.”
这是 C++ 社区最为推崇的设计理念之一。
例如:
- 使用构造函数和 RAII 自动管理资源
- 明确 API 的前提条件(如 noexcept、const、nullptr 安全)
- 减少使用者出错的可能性(不暴露裸指针等)
四、Prefer Exceptions to Error Codes
为什么?
- 错误码需要调用者显式检查,容易被忽视
- 异常自动传播,支持强保证
- 更易于构建可组合的 API
不该用异常的地方:
- 控制逻辑(如正常退出循环)
- 常规流程判断(如用户取消操作)
- 析构函数(绝对不要抛异常!)
五、异常安全的黄金法则集
原则 | 解说 |
---|---|
throw by value, catch by reference | 保留多态性,避免 slicing |
避免 dynamic exception specifications | 使用 noexcept |
析构函数绝不能抛异常 | 否则可能在 stack unwinding 时导致程序终止 |
使用 RAII 管理资源 | 构造即获得,析构即释放 |
所有清理代码放到析构函数中 | 确保总被执行 |
swap 要 noexcept | 否则无法保障 strong guarantee |
用 Critical Line 分隔可失败/不可失败代码 | 保持程序一致性 |
知道在哪 catch(Switch / Strategy / Some Success) | 合理处理异常 |
优先使用异常而非错误码 | 让 API 更安全,调用更清晰 |
总结一句话:
现代 C++ 编程要做的,是让正确的代码“容易写”,让错误的代码“难以编译”。
这整套体系就是围绕这个理念展开的:
- 强异常保证 = 稳定 & 可维护
- RAII + noexcept + swap = 安全、高效、现代
- 接口设计要符合 Scott Meyers 的黄金法则
C++ 异常安全与资源清理的实战技巧,非常实用。这一部分涵盖了:
on_scope_exit
自动清理机制- Lippincott 函数(异常归一化处理)
- 例外捕捉策略(特别是在 C 接口中)
下面我帮你详细逐条解读它们的 意义、用法与优点:
1. on_scope_exit
:作用域结束时自动执行清理操作
场景:
当一个函数中间可能会抛异常,你又必须在最后还原某些状态(如光标、锁、UI 状态等),你可以用:
esc::on_scope_exit handRestore(&UCursor::SetOpenHandCursor);
UCursor::SetGrabHandCursor();
// ... 做一些可能抛异常的事情 ...
这比你手动写 try/catch 或多个 return
前都放清理逻辑,要更安全、更简洁。
实现核心:
struct on_scope_exit {using exit_action_t = std::function<void(void)>;on_scope_exit(exit_action_t action): action_(action) {}~on_scope_exit() { if (action_) action_(); }void release() { action_ = nullptr; } // 可手动取消
private:exit_action_t action_;on_scope_exit(on_scope_exit const&) = delete;on_scope_exit& operator=(on_scope_exit const&) = delete;
};
优点:
- 自动清理,保障异常安全
- 适合老旧代码清理逻辑迁移
- 比 try/catch 更易维护
2. Lippincott Function:统一错误码生成
背景:
C++ 使用异常处理,但你常常要与 C 风格 API(或旧系统)集成,这些需要返回错误码。
示例前后对比:
原始方式(重复逻辑):
try {CodeThatMightThrow();
} catch (FrameworkException const& ex) {result = ex.GetErrorCode();
} catch (std::exception const&) {result = kUnknown;
}
使用 Lippincott 函数:
try {CodeThatMightThrow();
} catch (...) {result = ErrorFromException();
}
Lippincott 核心函数:
C_APIStatus ErrorFromException() {C_APIStatus result = kC_APIUnknownError;try {throw; // 重新抛出捕获的异常} catch (FrameworkException const& ex) {result = ex.GetErrorCode();} catch (Util::OSStatusException const& ex) {result = ex.GetStatus();}// ...其余 catch ...return result;
}
好处:
- 异常 → 错误码 的唯一转换点(集中处理)
- 维护方便,添加新异常类型更容易
- 避免重复代码
3. 应用场景 & 总结技巧
技术 | 适用场景 | 优点 |
---|---|---|
on_scope_exit | 自动清理 UI 状态、锁、文件句柄 | 简洁、安全 |
Lippincott 函数 | 异常 → 错误码(尤其是 C 接口需要返回错误码时) | 中央化错误映射,维护方便 |
boost::exception | 继承自 std::exception ,支持额外上下文 | 高级异常追踪 |
遗留代码过渡 | 在已有 try/catch、错误码基础上引入新方式 | 渐进式重构 |
Bonus:现代替代方案
std::unique_ptr
+ 自定义 deleter:资源自动释放std::scope_exit
(C++23):标准化的on_scope_exit
std::scope_exit guard([]{ restore_cursor(); });
总结一句话:
“异常安全不是可选项,而是稳定系统的基石。”
on_scope_exit
和 Lippincott 技巧,是现代 C++ 异常处理和资源管理的重要组成部分,让你可以更安全地处理复杂流程,尤其在与旧系统或 C 接口集成时。
如果你想,我可以帮你写一份完整示例代码,展示:
on_scope_exit
实际使用- Lippincott 异常处理组合
- 如何用现代 C++ 重构一个 legacy-style 的函数
如何将 C++ 旧代码(legacy code)安全地迁移到现代、异常安全的风格。我们继续总结与理解你这部分笔记的重点内容 —— 这正是现代 C++ 程序员面对遗留系统时的 实战指导方针。
核心思想:异常安全的渐进式重构
你摘录的内容是基于 Sean Parent 等 C++ 专家的实践经验,主要目的是:
使旧接口保持行为不变(即不抛异常)
新代码可以使用现代 C++ 风格(抛异常 + RAII)
在不中断现有系统的前提下逐步演化整个代码库
规则总结
Sean’s 3 条规则:
- 新代码必须具备异常安全性。
- 新的接口可以抛异常。
- 原有代码不允许改变接口行为(不能突然开始抛异常)。
这提供了一种 安全的、分层的迁移策略。
迁移策略 / Refactoring Steps
推荐方法:
- 新接口用现代方式实现(支持抛异常)
- 旧接口调用新实现,并包裹
try/catch (...)
- 确保旧接口仍然返回错误码,不抛异常
- 新代码可以自由调用新接口
- 等旧代码逐渐淘汰后,删除 wrapper
示意代码:
新接口(抛异常):
FileHandle OpenFile(std::string const& path) {// Throws std::ios_base::failure on error
}
老接口(包裹新接口):
FileHandle* C_API_OpenFile(const char* path, ErrorCode* err) {try {return new FileHandle(OpenFile(path));} catch (...) {*err = ErrorFromException();return nullptr;}
}
RAII 封装类(如 ACL)
通过 RAII 封装低层的资源结构,让异常自动释放资源,无需手动回收:
class ACL {
public:ACL(int entries) {handle_ = acl_init(entries);if (!handle_) throw std::runtime_error("ACL init failed");}~ACL() { if (handle_) acl_free(handle_); }acl_t get() const { return handle_; }acl_t release() {auto temp = handle_;handle_ = nullptr;return temp;}
private:acl_t handle_;
};
Before vs After 对比:
Before | After | |
---|---|---|
错误处理逻辑 | 繁琐嵌套的 if (result != 0) | throw /catch 分离逻辑与控制流 |
资源释放 | 手动 free() | 自动 RAII 管理 |
控制结构 | 过多 {} 、变量声明嵌套 | 简洁流畅,直线式“成功路径” |
可维护性 | 修改风险高,容易出错 | 模块清晰,可单元测试 |
可读性 | 较差 | 极佳(关注点单一) |
“成功路径”原则(Success Path Principle)
精神总结:
让代码只关注成功逻辑,错误处理自动化 & 分离出去!
这就是所谓的 “写代码时不要考虑错误处理” —— 并非忽视,而是用 C++ 语言特性(RAII + exception) 结构化处理错误,使代码主干干净、清晰。
为什么“100% Robust”?
- 所有错误都有异常处理,不会被漏掉
- 无需重复检查返回值,减少错误传播疏漏
- 所有资源由对象管理,不会泄漏
- 即使发生错误,系统保持一致状态(强异常保证)
最终愿景:统一设计风格
在过渡完成后:
- 原始 API 被淘汰
- 所有模块均支持异常
- 全部资源用 RAII 管理
- “错误代码时代”被封装在边界
Bonus:你可以尝试的小项目
如果你希望练手:
- 选一个项目中的“资源初始化 + 错误码返回”的函数
- 编写一个异常安全的
Wrapper
- 为资源写一个
RAII
封装类 - 将原函数重构为调用新代码的
try/catch
包装
我可以协助你一步步完成这个重构练习。如果你愿意,可以把一个旧的函数贴出来,我来帮你做第一版。