当前位置: 首页 > news >正文

现代C++:scope_guard 与 defer:通用作用域守卫

现代C++:scope_guard 与 defer:通用作用域守卫

仓库已经开源!仍然在持续建设中,喜欢的话点个⭐!相关的链接如下:

clone me!: git clone https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP

静态网页体验极大改进,点击这里直接阅览:https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/

在前面几篇我们讨论了智能指针——它们管理的是"资源的生命周期"(内存、文件句柄、socket 等)。但在实际工程中,还有一类场景:你需要在作用域退出时执行某个操作,但这个操作不一定是"释放资源"。它可能是恢复某个全局状态、提交或回滚一个事务、记录一条日志、通知某个监控组件。这种"退出时执行"的需求比资源管理更普遍、更灵活,而专门为资源管理设计的智能指针并不能很好地覆盖这些场景。

scope_guard(作用域守卫)就是为这类需求设计的通用工具。它的核心思想极其简单:把一个可调用对象绑定到一个栈对象的析构函数上——作用域退出时,自动调用。就这么朴素,但就这么有用。

scope_guard 的动机:不只是资源,还有状态回滚

我们先来看一个真实的场景:假设你在写一个配置修改函数,需要临时改变系统的运行模式,在操作完成后恢复原来的模式。如果函数只有一个 return 点,手动恢复没问题。但如果函数有多个 return path,或者中间可能抛出异常,手动恢复就会变得很脆弱。

// 没有 scope_guard 时的脆弱写法voidupdate_config(Config&cfg){Mode old_mode=get_current_mode();set_current_mode(kMaintenance);// 临时切换模式if(!validate(cfg)){set_current_mode(old_mode);// 恢复点 1return;}if(!apply(cfg)){set_current_mode(old_mode);// 恢复点 2return;}notify_observers();set_current_mode(old_mode);// 恢复点 3// 如果 notify_observers() 抛异常呢?忘了恢复!}

每次修改这个函数——添加新的 return path、增加可能抛异常的调用——你都得检查所有的"恢复点"有没有遗漏。随着函数越来越复杂,遗漏的概率趋近于 100%。

用 scope_guard 就简单多了:

voidupdate_config_guarded(Config&cfg){Mode old_mode=get_current_mode();set_current_mode(kMaintenance);// 作用域退出时自动恢复——不管怎么退出autorestore_mode=make_scope_guard([&]()noexcept{set_current_mode(old_mode);});if(!validate(cfg))return;// 自动恢复if(!apply(cfg))return;// 自动恢复notify_observers();// 即使抛异常也自动恢复}// 正常退出也自动恢复

restore_mode是一个 RAII 对象——它的析构函数会在作用域退出时调用那个 lambda。不管是return、异常传播、还是函数正常执行到末尾,恢复操作都会被执行。你只需要写一次恢复代码,再也不用担心遗漏。

实现一个通用的 ScopeGuard 类

scope_guard 的核心实现非常精简——一个模板类,包装一个可调用对象和一个 active 标志位。我们从最基础版本开始,逐步完善。

首先是核心实现:

#include<utility>#include<exception>#include<cstdlib>template<typenameF>classScopeGuard{public:explicitScopeGuard(F&&func)noexcept:func_(std::move(func)),active_(true){}ScopeGuard(ScopeGuard&&other)noexcept:func_(std::move(other.func_)),active_(other.active_){other.active_=false;}~ScopeGuard()noexcept{if(active_){try{func_();}catch(...){// 析构函数中绝不能让异常逃逸// 否则在栈展开过程中会导致 std::terminatestd::terminate();}}}// 取消守卫:成功后不需要执行清理voiddismiss()noexcept{active_=false;}// 禁止拷贝ScopeGuard(constScopeGuard&)=delete;ScopeGuard&operator=(constScopeGuard&)=delete;private:F func_;boolactive_;};template<typenameF>ScopeGuard<F>make_scope_guard(F&&func)noexcept{returnScopeGuard<F>(std::forward<F>(func));}

这个实现有几个值得注意的设计决策。析构函数用try-catch(...)包裹了func_()的调用,并在 catch 块中调用std::terminate()。在 C++ 标准中,如果析构函数在栈展开过程中抛出异常,程序会直接调用std::terminate()—— 毕竟运行时无法同时处理两个异常。虽然标注了noexcept的函数抛异常也会导致terminate()(这是编译器通过-Wterminate警告会提醒你的),但显式的 try-catch 给了我们一个将来添加日志或清理的机会。如果你对 noexcept 异常处理的行为不太确定,可以运行本章节的验证代码(06-scope-guard-verification.cpp)中的相关测试,实际观察一下 terminate 的触发时机。

dismiss()方法允许你在成功路径上取消守卫。这在"只在失败时回滚"的场景中非常有用——我们后面会看到更优雅的scope_fail实现。

defer 模式:Go 风格的延迟执行

Go 语言有一个defer关键字,它可以把一个函数调用延迟到当前函数返回时执行。这个特性在 Go 社区广受欢迎,因为它让"清理代码紧跟在获取代码后面"成为一种自然的编码风格。

虽然 C++ 没有语言级别的defer,但通过宏 +ScopeGuard可以实现非常接近的体验:

// 辅助宏:自动生成唯一变量名#defineSCOPE_GUARD_CONCAT_IMPL(x,y)x##y#defineSCOPE_GUARD_CONCAT(x,y)SCOPE_GUARD_CONCAT_IMPL(x,y)#defineSCOPE_GUARD_VAR(counter)SCOPE_GUARD_CONCAT(_scope_guard_,counter)// 使用 __COUNTER__ 保证每次生成唯一变量名// __COUNTER__ 是 GCC/Clang/MSVC 都支持的扩展#defineDEFER(code)\autoSCOPE_GUARD_VAR(__COUNTER__)=make_scope_guard([&]()noexcept{code;})// 备选方案:如果编译器不支持 __COUNTER__,用 __LINE__#defineDEFER_LINE(code)\autoSCOPE_GUARD_CONCAT(_scope_guard_,__LINE__)=\make_scope_guard([&]()noexcept{code;})

用法非常直观——DEFER后面跟一段代码,这段代码会在当前作用域退出时执行:

voidprocess_with_defer(){auto*region=allocate_region();DEFER({release_region(region);});auto*buffer=acquire_buffer();DEFER({release_buffer(buffer);});// 所有清理代码紧跟在获取代码后面// 不需要在函数末尾写一堆 release 调用do_processing(region,buffer);// 作用域退出时,buffer 先释放(后定义的先析构)// 然后 region 释放(先定义的后析构)}

DEFER宏的好处是把清理代码和获取代码放在了一起——读者不需要跳到函数末尾就能看到"这个资源会在什么时候释放"。这种局部性大大提高了代码的可读性和可维护性。

⚠️DEFER宏的 lambda 捕获了[&](引用捕获),这意味着它引用了外层作用域的局部变量。如果在DEFER执行时这些变量已经离开作用域,就会产生悬垂引用。不过在实际使用中,DEFER和它捕获的变量通常在同一个作用域内,所以这个问题很少出现——但你要意识到这个风险。如果确实需要跨作用域使用守卫对象,可以考虑按值捕获([=])或者确保守卫对象的生命周期不会超过被捕获的变量。

scope_success 和 scope_fail:区分成功与失败路径

有时候你只想在函数"正常返回"时执行某个操作(比如提交事务),或者只在"异常退出"时执行(比如回滚事务)。C++17 提供了std::uncaught_exceptions()来检测当前是否处于异常传播中——它返回当前正在传播但尚未被捕获的异常数量。基于这个信息,我们可以实现scope_successscope_fail

template<typenameF>classScopeSuccess{public:explicitScopeSuccess(F&&func)noexcept:func_(std::move(func)),active_(true),uncaught_at_creation_(std::uncaught_exceptions()){}~ScopeSuccess()noexcept{if(active_&&std::uncaught_exceptions()==uncaught_at_creation_){try{func_();}catch(...){std::terminate();}}}ScopeSuccess(ScopeSuccess&&other)noexcept:func_(std::move(other.func_)),active_(other.active_),uncaught_at_creation_(other.uncaught_at_creation_){other.active_=false;}voiddismiss()noexcept{active_=false;}ScopeSuccess(constScopeSuccess&)=delete;ScopeSuccess&operator=(constScopeSuccess&)=delete;private:F func_;boolactive_;intuncaught_at_creation_;};template<typenameF>classScopeFail{public:explicitScopeFail(F&&func)noexcept:func_(std::move(func)),active_(true),uncaught_at_creation_(std::uncaught_exceptions()){}~ScopeFail()noexcept{if(active_&&std::uncaught_exceptions()>uncaught_at_creation_){try{func_();}catch(...){std::terminate();}}}ScopeFail(ScopeFail&&other)noexcept:func_(std::move(other.func_)),active_(other.active_),uncaught_at_creation_(other.uncaught_at_creation_){other.active_=false;}voiddismiss()noexcept{active_=false;}ScopeFail(constScopeFail&)=delete;ScopeFail&operator=(constScopeFail&)=delete;private:F func_;boolactive_;intuncaught_at_creation_;};

原理是:在构造时记录当前的uncaught_exceptions()数量,在析构时比较——如果数量没变,说明没有新的异常被抛出(scope_success);如果数量增加了,说明有新的异常正在传播(scope_fail)。

⚠️ 注意使用std::uncaught_exceptions()(复数)而不是旧的std::uncaught_exception()(单数)。后者在嵌套 try-catch 的场景下行为不正确——它只能告诉你"有没有异常",而不能告诉你"有没有新的异常"。uncaught_exceptions()返回精确的数量,可以正确检测嵌套场景。旧的uncaught_exception()在 C++17 中已被弃用。

状态回滚示例:事务处理

scope_successscope_fail最经典的应用场景是事务处理——成功时提交,失败时回滚:

#include<iostream>#include<stdexcept>classDatabaseTransaction{public:voidbegin(){std::cout<<"BEGIN TRANSACTION\n";}voidcommit(){std::cout<<"COMMIT\n";}voidrollback(){std::cout<<"ROLLBACK\n";}};voidtransfer_money(DatabaseTransaction&tx,intfrom,intto,intamount){tx.begin();// 失败时自动回滚autoon_fail=ScopeFail<std::decay_t<decltype([]()noexcept{std::cout<<"自动回滚触发\n";})>>([]()noexcept{std::cout<<"异常导致自动回滚\n";});// 在实际项目中可以用辅助函数简化// auto on_fail = make_scope_fail([&]() noexcept { tx.rollback(); });if(amount<=0){throwstd::invalid_argument("amount must be positive");}std::cout<<"Transfer "<<amount<<" from "<<from<<" to "<<to<<"\n";// 成功时提交// auto on_success = make_scope_success([&]() noexcept { tx.commit(); });// 这里用 dismiss + 手动提交也是常见模式}voidtransaction_demo(){DatabaseTransaction tx;try{transfer_money(tx,1001,2002,-50);}catch(conststd::exception&e){std::cout<<"捕获异常: "<<e.what()<<"\n";}}

运行结果:

BEGIN TRANSACTION Transfer -50 from 1001 to 2002 异常导致自动回滚 ROLLBACK 捕获异常: amount must be positive

异常安全与 scope_guard

scope_guard 与异常安全的关系非常紧密。在 C++ 中,异常安全有三个级别(基本保证、强保证、不抛出保证),而 scope_guard 是实现强保证的重要工具。

考虑一个"先修改 A,再修改 B"的操作。如果 A 修改成功但 B 修改失败,我们需要回滚 A 以保证强异常安全:

voidupdate_both(SubsystemA&a,SubsystemB&b,constConfig&cfg){StateA old_a=a.get_state();a.update(cfg);// 可能抛异常// 为 A 设置回滚守卫autorollback_a=make_scope_guard([&]()noexcept{a.restore(old_a);// 如果后续操作失败,回滚 A});StateB old_b=b.get_state();b.update(cfg);// 如果这里抛异常,rollback_a 的析构会回滚 A// B 也成功了,取消 A 的回滚(如果需要也可以为 B 加守卫)rollback_a.dismiss();}

这种"先操作,失败则回滚"的模式在数据库操作、文件系统操作、网络协议实现中非常常见。scope_guard 让这种模式变得自然且不容易出错。

标准化进展:std::scope_exit 与 Boost.Scope

scope_guard 模式已经被 C++ 标准委员会注意到。Library Fundamentals TS v3(ISO/IEC TS 19568:2024)定义了三个作用域守卫类模板:std::experimental::scope_exit(作用域退出时执行)、std::experimental::scope_success(仅在正常退出时执行)和std::experimental::scope_fail(仅在异常退出时执行)。它们的行为与我们上面实现的基本一致,但标准化版本提供了更严格的异常安全保证和更完善的接口约束 —— 比如scope_exit的构造函数是noexcept的,并且不允许在构造时抛异常(否则会直接调用terminate())。

Boost 库也提供了 Boost.Scope,实现了类似的组件。如果你不想自己实现 scope_guard,可以直接使用 Boost.Scope 或者头文件-only 的 scope-lite 库(Martin Moene 编写,提供与标准提案兼容的接口,支持 C++98 起的编译器)。

在实际项目中,笔者通常的做法是:如果项目已经依赖 Boost,就用 Boost.Scope;如果不想引入 Boost 依赖,就用自己的轻量实现(就像我们今天写的那个ScopeGuard)。从功能完整性来看,我们的基础实现大约 40 行代码,已经覆盖了核心功能 —— 你可以运行06-scope-guard-verification.cpp看看它在多返回路径、异常处理、事务模式等场景下的实际表现。

验证代码

我们为本章节编写了完整的验证测试,你可以用它来验证 scope_guard 的各种行为:

# 编译(使用 g++)g++-std=c++17-Wall-Wextra-O2\code/volumn_codes/vol2/ch01-smart-pointers/06-scope-guard-verification.cpp\-o/tmp/06-scope-guard-verification# 运行/tmp/06-scope-guard-verification

验证代码包含以下测试用例:

  1. 基础 ScopeGuard—— 验证作用域退出时执行
  2. dismiss() 功能—— 验证取消守卫
  3. 多返回路径—— 验证提前 return 和正常退出都执行清理
  4. ScopeFail(异常时执行)—— 验证异常退出时触发
  5. ScopeFail(无异常时不执行)—— 验证正常退出不触发
  6. ScopeSuccess(正常时执行)—— 验证正常退出触发
  7. ScopeSuccess(异常时不执行)—— 验证异常退出不触发
  8. 事务模式—— 验证实际事务处理场景
  9. DEFER 宏模拟—— 验证资源释放顺序
  10. std::uncaught_exceptions() 行为—— 验证异常检测机制

这些测试覆盖了我们讨论的所有关键场景,你可以直接运行观察输出,也可以修改代码来测试边界情况。

小结

scope_guard 是 RAII 思想的通用化——不仅管理资源的获取和释放,还管理任何需要在作用域退出时执行的操作。通过把操作包装在一个栈对象的析构函数中,scope_guard 保证了不管控制流如何离开作用域(正常返回、提前 return、异常传播),操作都会被执行。

我们今天实现了三个守卫变体:ScopeGuard(总是执行)、ScopeSuccess(仅正常退出时执行)、ScopeFail(仅异常退出时执行),以及DEFER宏来提供 Go 风格的延迟执行语法。这些工具在事务处理、状态回滚、资源清理等场景中都能简化代码并提高可靠性 —— 你可以运行验证代码看看它们在实际场景中的表现。

这个章节到这里就告一段落了。从 RAII 到智能指针(unique_ptrshared_ptrweak_ptr),从自定义删除器到侵入式引用计数,再到通用的 scope_guard——我们完整地覆盖了现代 C++ 资源管理的核心工具链。掌握这些工具,就掌握了写出安全、高效、可维护的 C++ 代码的基础。

参考资源

  • cppreference: std::uncaught_exceptions
  • cppreference: Library Fundamentals TS v3 - scope_exit
  • Boost.Scope documentation
  • scope-lite: A single-header implementation
  • Andrei Alexandrescu,ScopeGuard, Dr. Dobb’s Journal, 2000
  • C++ Core Guidelines: Resource Management

相关阅读

  1. RVO 与 NRVO:编译器的返回值优化 - 相似度 75%
  2. 完美转发与移动语义实战 - 相似度 75%
  3. RAII 深入理解:资源管理的基石 - 相似度 67%
http://www.jsqmd.com/news/967963/

相关文章:

  • 用DGL和PyTorch复现HAN:手把手教你搞定异构图注意力网络(附完整代码)
  • 智能手机硬件架构深度解析:从基带原理到射频前端设计
  • 别再死记硬背MCMC了!用Python模拟一个会‘遗忘’的马尔可夫链,5分钟搞懂平稳分布
  • 番茄小说下载器终极指南:5分钟掌握全平台离线阅读与有声书生成
  • Windows与Linux文件互通革命:WinBtrfs驱动程序深度解析
  • 技术深度解析:BetterNCM Installer II - 网易云插件生态的革命性管理方案
  • 2026最新九江黄金回收白银回收铂金回收攻略,实地甄选五家优质实体店 - 诚金汇钻回收公司
  • SAP ABAP ALV表格编辑实战:手把手教你实现单元格联动更新与数据校验(含完整代码)
  • 越过“内存墙”,AI推理时代的晶圆级革命与算力路线
  • 搞懂这套公式,AI 视频不再崩!Ltx2.3-vrvb 提示词(Prompt)保姆级进阶指南
  • Calibre LVS报告解析:从错误定位到高效调试的完整指南
  • 从CAN调谐器到硅调谐器:射频前端芯片化演进与实战选型指南
  • 从IMDB电影推荐到DBLP学者分类:实战解析HAN模型在三大经典数据集上的表现
  • 半导体产业格局变迁与中国创业路径:从硅谷到张江的实战洞察
  • WinBtrfs终极指南:让Windows也能享受Linux文件系统的强大功能
  • 魔兽争霸3终极优化指南:免费解决Win10/Win11所有兼容性问题
  • 别再只看跑分了!用这5款免费工具,手把手教你全面看懂CPU真实性能
  • 2026年计划岗位SCMP资料试听课怎么领取?众智商学院官网400和冯老师 - 众智商学院官方
  • BetterNCM插件管理器技术方案:系统化解决网易云音乐功能扩展需求
  • 给GIS和游戏开发者的比喻:世界坐标(ECEF)和局部坐标(ENU)到底怎么理解?
  • Android Studio中文语言包架构优化:破解版本兼容性困境的3种技术方案
  • 晶振电路并联与串联电阻设计原理及调试指南
  • 通用GUI编程技术——图形渲染实战(四十八)——Owner-Draw控件:让标准控件焕然一新
  • 3分钟快速上手:FigmaCN中文汉化插件终极指南
  • 保姆级教程:用潘多拉/Pandvan固件搞定跨网段打印机共享(附端口转发避坑指南)
  • 基于STM32 HAL库的4×4矩阵键盘驱动工程(含CubeMX配置文件与MDK工程)
  • BetterNCM智能部署工具:让网易云音乐插件安装变得简单高效
  • 2026济南黄金回收白银回收铂金回收怎么变现?实地探访 5 家本地老牌回收店铺 - 中安检金银铂钻回收
  • 5G网络优化实战:如何通过SIB1消息参数精准定位UE接入失败问题(附排查清单)
  • 基于RT-Thread与W601 Wi-Fi MCU的物联网开发实战与生态解析