Google C++风格指南中全局非平凡析构对象在什么场景下有风险?
静态非平凡析构对象的风险场景解答
首先明确Google C风格指南这条规则的核心背景:C标准未定义跨翻译单元的静态存储期对象的构造顺序,而析构顺序为构造顺序的逆序,非平凡析构的对象执行完析构逻辑后就进入生命周期终止状态,访问这类对象属于未定义行为。
如果对象的析构函数是平凡的,其执行完全没有顺序要求(本质上不会被“运行”);否则我们会面临访问已结束生命周期对象的风险。因此我们仅允许静态存储期的对象为可平凡析构类型。基础类型(如指针、int)属于可平凡析构类型,由可平凡析构类型组成的数组同样符合要求。请注意被constexpr标记的变量均为可平凡析构。
你提到的错误示例const std::string kFoo = "foo";中,std::string的析构函数需要释放内部持有的堆内存,属于非平凡析构,会在以下常见场景触发安全风险:
- 跨翻译单元的静态对象依赖场景:如果
kFoo定义在A编译单元,另一个编译单元B中存在静态存储期对象,且该对象的析构函数需要读取kFoo的内容,由于两个静态对象的构造顺序无法确定,析构顺序也没有保证,大概率会出现kFoo先完成析构,B的静态对象析构时访问已销毁的kFoo,轻则触发程序崩溃,重则读到非法内存数据引发安全漏洞。 - 进程退出回调访问场景:如果你通过
atexit()、std::at_quick_exit()等接口注册了进程退出时执行的回调,且回调逻辑中访问了kFoo。静态对象析构和atexit回调的执行顺序和对象构造、回调注册的顺序强绑定,如果kFoo的构造时机晚于回调注册的时机,kFoo会先执行析构,回调触发时就会访问已失效的字符串对象。 - 动态库卸载场景:如果
kFoo定义在动态链接库中,动态库被卸载时会执行所有静态对象的析构,若此时主程序或者其他动态库还持有kFoo的指针/引用,后续访问就会触发悬空指针问题。
而可平凡析构的对象不需要执行任何析构逻辑,其内存会在进程完全退出后由操作系统统一回收,不存在“生命周期结束后状态非法”的问题,哪怕出现执行顺序偏差也不会触发未定义行为,因此是安全的。
内容的提问来源于stack exchange,提问作者Michal
相关产品推荐
相关产品推荐

