06 | 异常:用还是不用,这是个问题
06 | 异常:用还是不用,这是个问题
讲述:吴咏炜
时长13:17大小12.14M
没有异常的世界
使用异常
避免异常的风格指南?
异常的问题
使用异常的理由
内容小结
课后思考
参考资料
赞 7
提建议
精选留言(31)
- tt2019-12-09文中下面的一句话: “首先是内存分配。如果 new 出错,按照 C++ 的规则,一般会得到异常 bad_alloc,对象的构造也就失败了。这种情况下,在 catch 捕捉到这个异常之前,所有的栈上对象会全部被析构,资源全部被自动清理。” 谈的是new在分配内存时的错误,是堆上内存的错误,但自动被析构的却是栈上的对象。一开始我想是不是笔误了,但仔细想想,堆上的东西都是由栈上的变量所引用的,栈上对象析构的过程,堆上相应的资源自然就被释放了。而且被释放的对象的范围还被栈帧限定了。展开
作者回复: 对,这就是 RAII,非常重要。 学习速度飞快啊。👍
共 5 条评论32 - 亮2019-12-17看到老师说了部分开源的异常优秀的C++开源项目,老师能否推荐些现在流行的,能逐步深入的网络编程方面的C++开源项目看呢,从入门到深入的都推荐一些吧。谢谢老师
作者回复: 网络就看 Boost.Asio 吧。这个将是未来 C++ 网络标准库的基础。
28 - tokamak2019-12-13老师,你好。目前主流的开源项目中,有没有使用了异常的优秀的C++开源项目?可以用来作为参考案例。
作者回复: 我不觉得用异常有什么特别的地方,因而用异常的我个人没觉得有什么特别可参考的。 由于历史原因,有不少大名气的 C++ 程序没有使用异常,特别是 Google 的项目,比如 Chromium。不用异常,实际上是对用户友好(可执行文件略小,性能有可能有小提升),而对开发者更累。 我知道用到异常的一些项目: - Boost - C++ REST SDK - pytorch - pybind11 - Armadillo - nlohmann/json - cppcheck - OpenCV - LibreOffice 这篇文章也可以看一下: https://cppdepend.com/blog/?p=311
19 - fl2609197842020-02-17老师好,有没有编译器平台对异常的实现原理的资料呢,比如X86下G++ -S会发现安插了一些__cxa_throw之类的调用,感谢
作者回复: 可以参考下面这些链接: http://baiy.cn/doc/cpp/inside_exception.htm https://gcc.gnu.org/wiki/Dwarf2EHNewbiesHowto https://stackoverflow.com/questions/15670169/what-is-difference-between-sjlj-vs-dwarf-vs-seh
5 - EncodedStar2019-12-22一、使用异常 1.异常处理并不意味着需要写显示的try和catch。异常安全的代码,可以没有任何try和catch 2.适当组织好代码,利用好RAII,实现矩阵的代码和使用矩阵的代码都可以更短、更清晰,处理异常一般情况会记日志或者向外界用户报告错误。 二、使用异常的理由 1.vector C++标准容器中提供了at成员函数,能够在下标不存在的时候抛出异常(out_of_range),作为一种额外的帮助调试手段 2.强异常保证,就是一旦异常发生,现场会恢复到调用异常之前的状态。(vector在元素类型没有提供保证不抛异常的移动构造函数的情况下,在移动元素时会使用拷贝构造函数,一旦某操作发生异常,就可以恢复原来的样子) 3.只要使用标准容器就都的处理可能引发的异常bad_alloc 4。可以使用异常,也可以使用assert 课后思考 你的C++项目里使用过异常吗?为什么? 答:按老师课里说的,只要使用了标准容器就得考虑使用处理异常(bad_alloc),所以,大部分C++代码如果保证安全的情况下都的考虑这个异常。当然也在别的地方,之前在读取配置文件(json文件)字段的时候加过,如果读取失败,异常抛出展开
作者回复: OK。很好!
共 2 条评论5 - 中年男子2019-12-09用到异常的时候倒不是很多,但是异常千万别乱用,害人害己, 曾经同事离职,接手他项目的代码,把我坑的,几乎所有能引起crash的地方都用try catch 捕获异常,然而不处理异常,比如非法指针, 这种bug居然用try catch 来规避,坑了我两个月时间才把程序搞稳定了,现在想起他来,心里还有一句mmp想送给他。。。
作者回复: 任何东西用得不好都是坑。有朋友遇到小项目里用了一大堆(不必要的)设计模式,把代码硬生生弄得不可理解。不能说设计模式就是不好,是不? MSVC 可以用 ... 捕获非法指针操作,这也是极易被误用的功能。以前也遇到过一次,一不小心用了这个功能,把明明在调试时可以发现的崩溃变成了程序的怪异行为。不过,严格来讲这不属于 C++ 的异常……这实际上是 Windows 的 SEH,纯 C 里都能做得到。
共 3 条评论4 - 怪兽2021-06-09异常真是一个大话题,请假老师2个疑问点: 1. 函数标不标noexcept有什么区别?标了noexcept,表示不会抛出异常,也就表示异常安全吗?异常安全的代码也就不需要try和catch了 。但实际上,不管函数有没有标noexcept,如果确实抛出了异常,就会调用std::terminate。所以总的来说,函数标了noexcept只是一种声明而已,是想告诉编译器它是安全的,可以被move或其他优化,老师这样理解对吗? 2. 看评论说,析构函数缺省就是noexcept,那么构造函数缺省也是noexcept的吗?有必要标noexcept吗?展开
作者回复: 1. 对。标 noexcept 是一种契约声明,表明该函数永远不应该抛异常,提供不抛异常保证,比强异常安全保证更强。 2. 只有析构函数默认有 noexcept 声明(前提是所有的基类和成员变量的析构函数都 noexcept)。构造函数函数如果不是 default 声明的话,仍需手工标 noexcept。
3 - 怪兽2021-09-18老师打扰一下,请教2点疑惑: 1. 原文中描述:“C++ 的标准容器在大部分情况下提供了强异常保证,即,一旦异常发生,现场会恢复到调用函数之前的状态,容器的内容不会发生改变,也没有任何资源泄漏。” 既然是强异常保证下发生异常,此时不就立即调用std::terminate结束程序了?还需要在意现场有没有恢复?容器的内容有没有发生变化? 2. 原文中描述:“只要你使用了标准容器,不管你自己用不用异常,你都得处理标准容器可能引发的异常——至少有 bad_alloc” 这应该是容器在分配内存时,并不是异常安全的,才会抛出的bad_alloc异常吧?如果分配内存是异常安全的,当发生异常抛出bad_alloc时,就立即std::terminate结束程序了,这样理解对吗?展开
作者回复: 1. 强异常安全保证不是无异常保证,只是说发生异常时保证还原现场,对象不发生变化。 2. 基本同上。你似乎把异常安全和noexcept保证混淆了。 异常安全有四级: 不抛异常(noexcept)保证 强异常安全保证 基本安全异常保证 没有任何保证
2 - Minghao2020-05-27你好老师,学习了06异常和07迭代器,也自己写了一遍smart_pointer和istream_line_reader。您在课中提到了 “vector 会在元素类型没有提供保证不抛异常的移动构造函数的情况下,在移动元素时会使用拷贝构造函数“。我想请教一下,在自己开发的过程中,一般哪些成员函数需要考虑添加noexcept关键字呢?
作者回复: 最主要是移动构造函数、移动赋值函数和 swap。析构函数也不应该抛异常,但不用标,缺省就是 noexcept。其他函数,能确保没问题的也标一下,特别是很小的返回引用和指针的函数。
2 - 李蔚韬2019-12-09老师,对于异常的第一条批评我不太理解,什么叫“只要开启异常,即使不使用”,这里的开启是指什么呢?
作者回复: GCC/Clang 下的 -fexceptions(缺省开启),MSVC 下的 /EHsc(我要求大家需要用的,Visual Studio 项目里也会自动用)。 我刚试了,用 GCC,加上 -fno-exceptions 命令行参数,对于下面这样的小程序,也能看到产生的可执行文件的大小的变化。 #include <vector> int main() { std::vector<int> v{1, 2, 3, 4, 5}; v.push_back(20); }
共 5 条评论2 - 林林2021-03-26为什么说“异常处理并不意味着需要写显式的 try 和 catch” 没有catch的话,程序不是会挂掉吗?
作者回复: 是的,没有catch,程序会挂掉。但你需要写出 try 和 catch 的地方很少。很多地方的异常处理,就是让程序优雅地通过异常退出当前函数。 就像我文中描述的“matrix c = a * b;”这句可能出异常的地方有好几处,目前的代码写法,也在某种意义上“处理”了异常——确保发生异常时程序行为的完全正常,即给出了异常安全保证。 又如异常安全的代码,常常是让会抛异常的操作最先做(如内存分配),然后再做其他不会抛异常的操作。这样的代码,一般不需要写 try... catch,也同样能在异常情况执行正确的流程。 这和 Java 这样的在编译期进行大量异常检查的语言不一样。在 Java 里,因为检查性异常(checked exception)的使用,你通常会需要写出多得多的 try...catch 语句。
1 - geek2021-02-22老师,我理解RAII也并不是完全自动的,如果最顶层函数都没有try catch块的话,程序还是会异常结束,伴随着可能存在资源没释放的情况。RAII + 至少一个有效的try catch块 才能保证出现异常时,资源也会正确释放吧。
作者回复: 你说是个标准没有规定的实现细节。如果程序的异常完全没有捕获,程序就会挂。要挂的程序,还要清理啥?所以大部分实现就完全不清理了。
1 - hammond2020-09-07析构函数没法抛出异常吗?
作者回复: 可以,但不应该。先记住基本规则,不要在析构函数里抛异常——除非你是C++专家,知道所有的语法细节和特殊处理逻辑,知道你为什么要打破规则。
1 - Fiery2020-03-23习惯了python和golang支持的多返回值,会感觉返回错误码很方便,只要把错误码作为第二个返回值就好了,只不过c++默认只支持一个返回值,想返回多个值还要显示的调用std::tie,要是可以直接支持多返回值的话,就不会这么纠结了吧?
作者回复: 返回错误码在C++里有很多方案的,继续往下看吧(第8讲和第22讲)。 Python和Go还是有明显区别的。两者都支持很方便地返回多个值,但Python里的错误处理仍然是异常为主。 使用错误码,会让代码更啰嗦,对代码的整体设计要求更高,使用上不及异常方便。
1 - 灯盖2023-02-07 来自北京项目中有使用异常。多层函数的嵌套,异常信号的传递都是在这个过程中需要考虑的问题。异常,bool返回值,错误码,assert这些手段目前在项目中都有存在。怎样是一个比较好的状态,还需要摸索。
作者回复: 核心关注:满足项目的基本约束(性能、空间等);开发者易于使用;处理方式具有一致性(实际也是易用性的一种体现)。
- anotherS2022-06-14"1.异常处理并不意味着需要写显式的 try 和 catch。2.异常安全的代码,可以没有任何 try 和 catch。" 这个可以举个例子吗? 1. 不显示的写是说还有隐式的什么操作吗? 2. 异常安全的代码,异常安全指在发生异常时,既不会发生资源泄漏,系统也不会处于一个不一致的状态。但是还是有异常发生并且抛出了,如果没有try cache ,那不是崩了?崩了都不try吗?展开
作者回复: 这是 libc++ 里的 shared_ptr 的一个构造函数: explicit shared_ptr(_Yp* __p) : __ptr_(__p) { unique_ptr<_Yp> __hold(__p); typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT; typedef __shared_ptr_pointer<_Yp*, __shared_ptr_default_delete<_Tp, _Yp>, _AllocT > _CntrlBlk; __cntrl_ = new _CntrlBlk(__p, __shared_ptr_default_delete<_Tp, _Yp>(), _AllocT()); __hold.release(); __enable_weak_this(__p, __p); } 如果使用 try/catch,可以写成下面的样子: explicit shared_ptr(_Yp* __p) : __ptr_(__p) { typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT; typedef __shared_ptr_pointer<_Yp*, __shared_ptr_default_delete<_Tp, _Yp>, _AllocT > _CntrlBlk; try { __cntrl_ = new _CntrlBlk(__p, __shared_ptr_default_delete<_Tp, _Yp>(), _AllocT()); } catch (std::bad_alloc&) { delete __p; throw; } __enable_weak_this(__p, __p); } 能用 RAII 的时候,用 RAII 来保证异常安全。外层当然还是需要有个地方 try/catch 来做真正的错误处理的,但和错误码的情况不同,你在大部分的代码里不用显式处理错误。
- jcy2022-04-13明白了,大概理解是使用异常代码更简洁优雅,非特殊场景使用异常更好 另 第一次定义 class matrix 的地方最后少了个 ; 号
作者回复: 谢谢。已修正。
- 怪兽2021-06-11老师又打扰一下,原文中“特殊成员(构造函数、析构函数、赋值函数等)会自动成为 noexcept,如果它们调用的代码都是 noexcept 的话。”那么返过来呢?例如赋值函数标noexcept,而赋值函数中调用的swap函数没有noexcept,那么最终会是什么效果?
作者回复: 我这儿写得不太好,应该说自动生成的特殊成员函数会是 noexcept…… 如果一个标成 noexcept 的函数中抛出了异常,编译器会自动产生一个对 std::terminate 的调用,终止程序运行。这个是运行期行为。编译时编译器不会对你调用非 noexcept 的函数有任何指示。
- Geek_68d3d22020-06-08总结:程序在编写过程中总会有不正常的情况,处理不正常的情况要么使用错误码各种if/else 还要预防在构造函数里出现不正常现象,要么使用异常,在异常出现后进行try catch最终保证程序无内存泄漏且功能一致。
作者回复: 对。 错误码的主要好处是代码直白(但啰嗦),开销低。异常代码更优雅,但需要更多的训练。另外,如果没有捕获异常的话,异常在GCC和Clang上都可以产生较友好的运行时错误,方便调试。
共 3 条评论 - JDY2020-02-26老师您好,我请教一个很基础的题外问题,在这个初始化函数中,我们为什么不传递一个matrix ptr,然后用ptr.data 呢?很小的问题,希望您能回答一下,谢谢了~ int matrix_alloc(matrix* ptr, size_t nrows, size_t ncols){ ..... ptr -> data = data; }展开
作者回复: 没看懂你的问题。文中的 matrix_alloc 是初始化的代码,传的就是 matrix 的指针。在 matrix 初始化之前,data 成员的内容是未知的垃圾数据。所以初始化时,直接覆写 matrix 的所有成员。C 的处理方式就是这么原始的。