C++标准6.7.3.5中“依赖析构副作用”含义的技术问询
对C++20标准中析构函数未调用相关未定义行为的解读困惑
程序可通过重用对象占用的存储、显式调用对象的析构函数或伪析构函数(7.5.4.4)来终止任意对象的生存期。对于类类型的对象,程序无需在重用或释放其占用的存储前显式调用析构函数;但如果未显式调用析构函数,也未使用delete表达式(7.6.2.9)释放存储,则析构函数不会被隐式调用,任何依赖该析构函数产生的副作用的程序都存在未定义行为(undefined behaviour)。
引用自ISO/IEC 14882:2020(E)(C++20标准)
我对上述段落最后一句话存在困惑,它是典型的“若X则Y”结构,Y是严重的未定义行为,但X的含义不明确。目前我有两种合理解读:
- 解读1:缺少析构函数调用时,可观察副作用不应发生变化(类似“as-if”规则的思路)
- 解读2:如果其他代码需要依赖“析构函数被调用”的假设来保证程序良构,那么不调用析构函数会导致未定义行为
针对解读1的验证分析
编写了测试代码,编译选项为-std=c++20 -O3 -fsanitize=undefined,分别在常量求值上下文和运行时执行测试逻辑:
- 在常量求值函数
f()中,创建对象s并调用print_buffered()标记需要打印,随后通过std::construct_at(&s)终止原对象生存期并构造新对象,全程未调用原对象的析构函数 - 原对象的析构函数若被执行,会产生向标准输出打印内容的可观察副作用
- 通过
static_assert(f() == 12)强制常量求值上下文执行f(),同时在main()中执行相同逻辑用于检测未定义行为
测试结果:static_assert未触发,clang和gcc均未将该情况判定为未定义行为;运行时执行时,未定义行为 sanitizer也未捕获问题。这说明要么该情况确实不属于未定义行为,要么编译器存在问题;若为前者,则可作为反对解读1的证据。此外,这种情况本应可被诊断,编译器可尝试分析析构函数是否存在可观察副作用。
针对解读2的分析
该解读在某种意义上显得冗余,但并非严重问题。我目前倾向于第二种解读,但不确定是否正确。我未在标准中找到“depends”的明确定义,日常语境中我不认为示例程序依赖析构副作用,我认为该词适用于“不调用析构函数会破坏程序正确性关键不变量”的场景。
测试代码
#include <new> #include <iostream> struct S { private: bool m_print; public: constexpr S() : m_print(false){} S(const S&) = delete; S(S&&) = delete; S& operator=(const S&) = delete; S& operator=(S&&) = delete; constexpr void print_buffered(){m_print = true;} constexpr ~S(){ if (m_print) std::cout << "HELLO WORLD" << std::endl; } }; constexpr int f(){ S s; s.print_buffered(); std::construct_at(&s); // placement new not usable in constant-evaluated contexts :' return 12; } static_assert(f() == 12); int main(){ std::cout << "Hi\n"; S s; s.print_buffered(); new (&s) S; // this ends lifetime of object and starts lifetime of new object std::construct_at(&s); // ^ same }
内容的提问来源于stack exchange,提问作者Ivan Lazaric
相关产品推荐
相关产品推荐

