故事
稣看到一些代码使用手动方式管理资源,便打算安利《Boost【2】ScopeExit》减少心智负担,然而并非所有团队都能立刻接受 Boost 这么大的开发库,于是先推荐 GSL。
结果被问了这么一个问题:
案例分析
1. 手动管理
假设有一种资源由 C 代码管理,还有一个可能抛出异常的函数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| extern "C" {
#include <stdio.h>
void init() { printf("%s\n", __func__); }
void uninit() { printf("%s\n", __func__); }
}
void something_may_throw() { std::cout << __func__ << '\n'; throw std::exception("Bad news!"); }
|
那么手动管理的代码可能类似这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void scope1() { std::cout << __func__ << '\n';
try { init(); something_may_throw(); uninit(); } catch (std::exception& ex) { std::cout << ex.what() << '\n'; }
std::cout << '\n'; }
|
它的实际运行结果将是:
1 2 3 4
| scope1 init something_may_throw Bad news!
|
八哥在于 uninit 漏调用了!结论:手动管理是有心智负担的!
2. 使用智能指针
利用自定义智能指针的 deleter 来实现自动调用 uninit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void scope2() { std::cout << __func__ << '\n';
try { init(); std::shared_ptr<void> _(nullptr, [](void* p) -> void { uninit(); }); something_may_throw(); } catch (std::exception& ex) { std::cout << ex.what() << '\n'; }
std::cout << '\n'; }
|
运行结果:没有资源泄漏!
1 2 3 4 5
| scope2 init something_may_throw uninit Bad news!
|
但它有两个问题:
使用 Compiler Explorer 查看以上智能指针编译出来的汇编行数,就知道有多污染眼睛!
另外提醒,以下 unique_ptr 版本无法达到效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void scope2u() { std::cout << __func__ << '\n';
try { init(); std::unique_ptr<void, void(*)(void* p)> _(nullptr, [](void* p) -> void { uninit(); }); something_may_throw(); } catch (std::exception& ex) { std::cout << ex.what() << '\n'; }
std::cout << '\n'; }
|
3. 使用 gsl::final_action
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void scope3() { std::cout << __func__ << '\n';
try { init(); gsl::final_action _{ [] { uninit(); } }; something_may_throw(); } catch (std::exception& ex) { std::cout << ex.what() << '\n'; }
std::cout << '\n'; }
|
运行结果同样完美无泄漏:
1 2 3 4 5
| scope3 init something_may_throw uninit Bad news!
|
并且可读性更好,其对应的汇编也更为简洁。
结论
C++ 是追求尽量降低抽象成本的,显然在这种场景下使用智能指针不如 Boost.ScopeExit 或 gsl::final_action 合适。