可观察行为和未定义行为 - 如果我不调用析构函数会发生什么?

Meh*_*dad 24 c++ destructor undefined-behavior language-lawyer

注意:我见过类似的问题,但没有一个答案是准确的,所以我自己也会这样问.

这是一个非常挑剔的"语言 - 律师"问题; 我正在寻找权威的答案.

C++标准说:

程序可以通过重用对象占用的存储来结束任何对象的生命周期,或者通过使用非平凡的析构函数显式调用类类型的对象的析构函数来结束任何对象的生命周期.对于具有非平凡析构函数的类类型的对象,程序不需要在重用或释放对象占用的存储之前显式调用析构函数; 但是,如果没有对析构函数的显式调用或者如果没有使用delete-expression来释放存储,则不应该隐式调用析构函数,并且任何依赖于析构函数产生的副作用的程序都有未定义的行为.

我根本不明白"取决于副作用"是什么意思.

一般问题是:

是忘记调用析构函数而不是忘记使用同一个函数调用普通函数?

举例说明我的观点是:

考虑下面这样的程序.还要考虑明显的变化(例如,如果我不在另一个上构造一个对象,但我仍然忘记调用析构函数,如果我打印输出来观察它,等等):

#include <math.h>
#include <stdio.h>

struct MakeRandom
{
    int *p;
    MakeRandom(int *p) : p(p) { *p = rand(); }
    ~MakeRandom() { *p ^= rand(); }
};

int main()
{
    srand((unsigned) time(NULL));        // Set a random seed... not so important
    // In C++11 we could use std::random_xyz instead, that's not the point

    int x = 0;
    MakeRandom *r = new MakeRandom(&x);  // Oops, forgot to call the destructor
    new (r) MakeRandom(&x);              // Heck, I'll make another object on top
    r->~MakeRandom();                    // I'll remember to destroy this one!
    printf("%d", x);                     // ... so is this undefined behavior!?!
    // If it's indeed UB: now what if I didn't print anything?
}
Run Code Online (Sandbox Code Playgroud)

我说这显示出"未定义的行为"似乎是荒谬的,因为x它已经是随机的 - 因此,对它进行异或,另一个随机数不能真正使程序比以前更"未定义",可以吗?

此外,在什么时候说程序"取决于"析构函数是正确的?如果值是随机的,它是否会这样做 - 或者一般情况下,如果我无法区分析构函数与运行与未运行?如果我从未读过这个价值怎么办?基本上:

在哪个条件(如果有)下,此程序是否显示未定义的行为?

究竟是哪个表达式或语句引起了这个,为什么?

Nik*_* C. 7

我根本不明白"取决于副作用"是什么意思.

这意味着它取决于析构函数正在做的事情.在您的示例中,修改*p或不修改它.您的代码中存在依赖关系,因为如果不调用dctor,输出会有所不同.

在当前代码中,打印的数字可能与第二次rand()调用返回的数字不同.你的程序会调用未定义的行为,但这只是UB在这里没有任何不良影响.

如果您不打印该值(或以其他方式读取它),则不会对dcor的副作用产生任何依赖性,因此也不会有UB.

所以:

是忘记调用析构函数而不是忘记使用同一个函数调用普通函数?

不,在这方面没有任何不同.如果你依赖它被调用,你必须确保它被调用,否则你的依赖不满足.

此外,在什么时候说程序"取决于"析构函数是正确的?如果值是随机的,它是否会这样做 - 或者一般情况下,如果我无法区分析构函数与运行与未运行?

随机与否并不重要,因为代码取决于要写入的变量.仅仅因为很难预测新值是什么并不意味着没有依赖性.

如果我从未读过这个价值怎么办?

然后没有UB,因为代码在写入之后对变量没有依赖性.

在哪个条件(如果有)下,此程序是否显示未定义的行为?

没有条件.它始终是UB.

究竟是哪个表达式或语句引起了这个,为什么?

表达方式:

printf("%d", x);
Run Code Online (Sandbox Code Playgroud)

因为它引入了对受影响变量的依赖性.

  • @Mehrdad标准没有说明允许做什么以及UB不支持什么.但是你可以假设它在这种情况下会做什么.在这种情况下,崩溃似乎非常遥远.但是,你的问题不在于UB是什么.这是关于这个特定的节目是否展示UB,我相信,我回答了这个问题.UB的意思完全是另一个问题.如果您想询问,请打开一个新的. (6认同)
  • @Mehrdad标准并没有说它可能会崩溃.这只是我们通常用来向其他人解释UB的东西.可以安全地假设这不会崩溃.您忘记了标准不是编译器.这只是纸上的文字. (3认同)
  • @Mehrdad总是可以为UB构建一个没有任何不良影响的测试用例.更重要的问题是你是否可以构造一个*做*的例子.或者更重要的是,不同的或更新的编译器是否可能表现出不同的行为. (3认同)

Gen*_*ene 5

如果您接受标准要求在析构函数影响程序行为的情况下通过销毁来平衡分配,则这是有意义的.即唯一可信的解释是,如果一个程序

  • 没有成功地调用析构函数(或者间接通过delete)的物体上,并
  • 说析构函数有副作用,

然后该计划注定了UB的土地.(OTOH,如果析构函数不影响程序行为,那么你就可以了.你可以跳过这个电话.)

注意添加副作用在本SO文章中讨论,我在不再重复.一个保守的推论是"程序......依赖于析构函数"相当于"析构函数具有副作用".

附加说明然而,该标准似乎允许更自由的解释.它没有正式定义程序的依赖性.(它确实将表达的特定质量定义为依赖性,但这不适用于此.)然而,在"A依赖于B"和"A依赖于B"的衍生物的100多种用法中,它采用了传统的这个词的意义:B的变化直接导致A的变化.因此,推断程序P依赖于副作用E似乎没有一个飞跃,以至于E的表现或不表现导致变异在执行P期间的可观察行为中.在这里,我们坚实的基础.程序的含义 - 它的语义 - 在标准下等同于其在执行期间的可观察行为,并且这是明确定义的.

符合实施的最低要求是:

  • 严格根据抽象机器的规则来评估对volatile对象的访问.

  • 在程序终止时,写入文件的所有数据应与根据抽象语义产生的程序执行的可能结果之一相同.

  • 交互设备的输入和输出动态应以这样一种方式进行,即在程序等待输入之前提示输出实际被传送.构成交互设备的是实现定义的.

这些统称为程序的可观察行为.

因此,根据标准的约定,如果析构函数的副作用最终会影响易失性存储访问,输入或输出,并且从不调用析构函数,则程序具有UB.

换句话说:如果你的析构函数做了重要的事情并且没有被一直调用,你的程序(标准说)应该被考虑,并且在此声明,没用.

对于语言标准,这是否过于严格,可能是迂腐?(毕竟,标准防止发生副作用,由于隐式析构函数调用,然后drubs你,如果析构函数导致可观察行为的变化,如果它已经被称为!)也许是这样.但它作为一种坚持良好形成的计划的方式确实有意义.


Sea*_*ean -1

假设您有一个类在其构造函数中获取锁,然后在其析构函数中释放锁。释放锁是调用析构函数的副作用。

现在,您的工作是确保调用析构函数。通常,这是通过调用 来完成的delete,但您也可以直接调用它,如果您使用placement new 分配了一个对象,通常会这样做。

在您的示例中,您分配了 2 个MakeRandom实例,但只调用了其中一个实例的析构函数。如果它正在管理某些资源(例如文件),那么您就会遇到资源泄漏。

所以,回答你的问题,是的,忘记调用析构函数与忘记调用普通函数不同。析构函数是构造函数的逆函数。您需要调用构造函数,因此您需要调用析构函数才能“展开”析构函数所做的任何操作。“普通”函数的情况并非如此。