注意:我见过类似的问题,但没有一个答案是准确的,所以我自己就是这样问。
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?
}
我觉得这显示出“未定义的行为”似乎很荒谬,因为x
已经是随机的 - 因此对另一个随机数进行异或不能使程序比以前更“未定义”,是吗?
此外,在什么时候说程序“依赖”析构函数是正确的?如果值是随机的,它是否会这样做 - 或者一般情况下,如果我无法区分析构函数与运行与未运行?如果我从未读过这个值怎么办?基本上是:
究竟是哪个表达式或语句导致了这个,以及为什么?
答案 0 :(得分:7)
我根本不明白“取决于副作用”是什么意思。
这意味着它取决于析构函数正在做的事情。在您的示例中,修改*p
或不修改它。您的代码中存在依赖关系,因为如果不调用dctor,输出会有所不同。
在当前代码中,打印的数字可能与第二次rand()调用返回的数字不同。你的程序会调用未定义的行为,但这只是UB在这里没有任何不良影响。
如果您不打印该值(或以其他方式阅读),则不会对dcor的副作用产生任何依赖性,因此也不会有UB。
所以:
忘记调用析构函数与忘记调用具有相同正文的普通函数有什么不同?
不,在这方面没有任何不同。如果你依赖它被调用,你必须确保它被调用,否则你的依赖不满意。
此外,在什么时候说程序“依赖”析构函数是正确的?如果值是随机的,那么它是否会这样做 - 或者一般来说,如果我无法区分析构函数与运行而不是运行?
随机与否并不重要,因为代码取决于要写入的变量。仅仅因为很难预测新值是什么并不意味着没有依赖性。
如果我从未读过该值,该怎么办?
然后没有UB,因为代码在写入后没有依赖于变量。
在哪个条件(如果有)下,此程序是否显示未定义的行为?
没有条件。它总是UB。
究竟是哪个表达式或语句导致了这个,以及为什么?
表达式:
printf("%d", x);
因为它引入了对受影响变量的依赖性。
答案 1 :(得分:5)
如果您接受标准要求分配在析构函数影响程序行为的情况下通过销毁进行平衡,则这是有意义的。即唯一可信的解释是,如果一个程序
delete
间接调用)
注意添加副作用在this SO article中讨论,我在此不再重复。一个保守的推论是“程序......依赖于析构函数”相当于“析构函数具有副作用”。
附加说明然而,标准似乎允许更自由的解释。它没有正式定义程序的依赖性。 (它确实将表达的特定质量定义为依赖性,但这不适用于此。)然而,在“A依赖于B”和“A依赖于B”的衍生物的100多种用法中,它采用了传统的这个词的意义:B的变化直接导致A的变化。因此,推断程序P依赖于副作用E似乎没有一个飞跃,以至于E的性能或不执行导致变化在执行P 期间的可观察行为中。在这里,我们坚实的基础。程序的含义 - 它的语义 - 在标准下等同于其在执行期间的可观察行为,并且这是明确定义的。
符合实施的最低要求是:
严格按照抽象机的规则评估对易失性对象的访问。
在程序终止时,写入文件的所有数据应与根据抽象语义生成的程序执行的可能结果之一相同。
交互设备的输入和输出动态应以这样的方式进行 提示输出实际上是在程序等待输入之前传递的。什么构成了 交互设备是实现定义的。
这些统称为程序的可观察行为。
因此,根据标准的约定,如果析构函数的副作用最终会影响易失性存储访问,输入或输出,并且从不调用析构函数,则程序具有UB。 < / em>的
换句话说:如果你的析构函数做了重要的事情并且没有被一直调用,你的程序(标准说)应该被考虑,并且在此声明,没用。
对于语言标准来说,这是否过于严格,可能是迂腐吗? (毕竟,标准阻止由于隐式析构函数调用而发生副作用,然后如果析构函数会导致可观察行为发生变化,那么就会扼杀你已被调用!)也许如此。但它作为一种坚持良好形成的计划的方式确实有意义。
答案 2 :(得分:4)
这在标准中确实不是一个非常明确的事情,但我会将“依赖于”解释为“抽象机器规则下的行为受到影响”。
此行为包括对volatile变量的读写顺序以及对库I / O函数的调用(至少包括标准库的I / O函数,如printf
,但也可能包括任何给定实现中的任何数量的附加函数,例如WinAPI函数)。有关确切的措辞,请参见1.9 / 9.
因此,如果执行析构函数或缺少析构函数会影响此行为,则行为未定义。在你的例子中,析构函数是否被执行会影响x
的值,但是无论如何该存储都已经死了,因为下一个构造函数调用会覆盖它,所以编译器实际上可以将它优化掉(并且可能是将)。但更重要的是,对rand()
的调用会影响RNG的内部状态,这会影响rand()
在另一个对象的构造函数和析构函数中返回的值,因此它会影响{{1}的最终值}}。无论哪种方式,它都是“随机的”(伪随机),但它会是一个不同的值。然后打印x
,将该修改转换为可观察的行为,从而使程序未定义。
如果你从未做过x
或RNG状态可观察到的任何事情,那么可观察行为将不会改变,无论是否调用析构函数,因此它不会被定义。
答案 3 :(得分:3)
对于这个答案,我将使用C ++标准的2012 C ++ 11版本,可以找到here (C++ standard),因为它是免费提供的并且是最新的。
您问题中使用的以下三个术语如下:
取决于
让我们采取一个&#34;维权法官&#34;方法,并假设&#34;取决于&#34;,&#34;依赖&#34;和&#34;取决于&#34;所有这些都用在本文档的类似上下文中,也就是说,该语言用于表达一个广泛的想法,而不是传达一个legalease概念。
然后我们可以分析第1194页的这一部分:
17.6.3.2
影响原始功能:功能交换移动到不同的标题
理由:删除对swap的依赖。
对原始功能的影响:编译期望的有效C ++ 2003代码 交换到&lt;算法&gt;可能不得不包括&lt;实用程序&gt;。
这部分表示严格的依赖性;你最初需要包含来获得std :: swap。 &#34;取决于&#34;因此,在没有要求继续下去的情况下,没有足够的背景,这表明了严格的要求,这是必要的。没有依赖性就会发生失败。
我选择了这段经文,因为它尽可能清楚地表达了意图;其他段落更冗长,但它们都包含类似的含义:必要性。
因此,&#34;取决于&#34;关系意味着依赖项目需要依赖的东西才有意义,完整和完整,并且可以在上下文中使用。
要切断法律术语繁文缛节,这意味着A取决于B意味着A需要B.这基本上是您所理解的&#34;依赖&#34;这意味着如果你在字典中查找它或用句子说出来。
副作用
这是更严格定义的,第10页:
访问由volatile glvalue(3.10)指定的对象,修改对象,调用库I / O. 函数或调用执行任何这些操作的函数都是 side e ff ects ,这些是 执行环境的状态。
这意味着任何导致环境变化的因素(如RAM,网络IO,变量等)都是副作用。这完全符合函数式语言中杂质/纯度的概念,这显然是预期的。请注意,C ++标准不要求可观察到这样的副作用;以任何方式修改变量,即使从未查看过该变量,仍然是副作用。
然而,由于&#34;好像&#34;规则,这种不可观察的副作用可能会被删除,第8页:
执行格式良好的程序的符合实现应产生相同的可观察行为 作为具有相同程序的抽象机的相应实例的可能执行之一 和相同的输入。但是,如果任何此类执行包含未定义的操作,则为此国际 标准不要求使用该输入执行该程序的实现(甚至不是 关于第一次未完成的操作之前的操作。)
取决于副作用
将这两个定义放在一起,我们现在可以定义这个短语:当需要对执行环境进行这些更改以满足程序的有意义的,完整的和完整的操作时,某些东西取决于副作用。如果没有副作用,程序以标准兼容方式运行所需的某些约束不满足,我们可以说取决于副作用。
如另一个答案中所述,一个简单的例子可以说明这个问题。使用锁的程序取决于锁的副作用,特别是,为某些资源提供序列化访问模式的副作用(简化)。如果违反了这种副作用,则违反了程序的限制,因此程序不能被认为是有意义的(因为可能会出现竞争条件或其他危险)。
程序通过副作用取决于锁提供的约束;在程序无效的情况下违反这些结果。
取决于析构函数产生的副作用
将语言从引用锁定转换为析构函数很简单明了;如果析构函数具有满足某些约束的副作用,该约束是程序所要求的有意义的,完整的,完整的和可用的,那么它取决于析构函数产生的副作用。这并不是很难理解,并且非常容易从标准的法律解释和粗略的外行理解这些词语以及如何使用它们开始。
现在我们可以回答你的问题:
在哪个条件(如果有)下,此程序是否显示未定义的行为?
如果由于未调用析构函数而未满足依赖项或要求,则任何相关代码的行为都是未定义的。但这究竟意味着什么?
1.3.24 未定义的行为
本国际标准没有要求的行为[注意:当本国际标准遗漏任何明确的定义时,可能会出现未定义的行为 行为或程序使用错误的构造或错误数据时。
允许的未定义行为 范围从完全忽略情况与不可预测的结果,到翻译期间的行为或 程序以环境特征的文件形式执行(有或没有发行 诊断消息),终止翻译或执行(发布诊断消息)。
许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。 - 结束说明]
让我们暂时想一下WAS定义的这种行为。
假设它明显是非法的。然后,这将需要任何标准编译器来检测这种情况,以诊断它,以某种方式处理它。例如,任何未显式删除的对象都必须在程序退出时删除,这需要某种跟踪机制和向任意类型发出析构函数的能力,这可能在编译时是未知的。这基本上是一个垃圾收集器,但考虑到它可能隐藏指针,可以调用malloc等等,要求它本身是不可行的。
假设明确允许。这也允许编译器在as-if规则下删除析构函数调用,因为嘿,你无论如何都不能依赖于这种行为。这会导致一些令人讨厌的惊喜,主要与内存不能快速或轻松地释放有关。为了解决这个问题,我们都开始使用终结器,问题又出现了。此外,允许该行为意味着没有库可以确定它们的内存何时被恢复,或者它是否曾经存在,或者它们的锁,OS依赖资源等是否会被返回。这会将使用资源的代码从代码中清除的要求推到提供它的代码中,在这种情况下,用C或C ++这样的语言基本上无法处理。
假设它有特定的行为;这会是什么行为?任何此类行为都必须非常复杂,或者不能涵盖大量案件。我们已经涵盖了两个,并且在程序退出时清理任何给定对象的想法会产生很大的开销。对于一种快速或至少是最小的语言,这显然是一种不必要的负担。
所以相反,行为被标记为 undefined ,这意味着任何实现都可以免费提供诊断,但也可以自由地忽略问题并留给您解决。 但无论如何,如果您依赖于满足这些约束但未能调用析构函数,那么您将获得未定义的行为。即使该程序运行良好,该行为也未定义;它可能会在一些新版本的Clang中抛出错误消息,它可能会在遥远的未来某些令人难以置信的安全加密操作系统中删除您的硬盘,它可能会工作到时间结束。
但它仍未定义。
您的示例
你的例子不满足&#34;取决于&#34;条款;没有满足程序运行所需的约束。
否则此程序中x的某个值或缺少该值会导致约束不满意;你没有调用未定义的行为。没有什么&#34;取决于&#34;这些副作用;如果要添加一个作为约束的测试,该约束需要某个值为&#34; x&#34;,那么它将是未定义的行为。
就目前而言,你的例子不是未定义的行为;它只是错了。
最后!
忘记调用析构函数与忘记调用具有相同正文的普通函数有什么不同?
在许多情况下,使用相同的主体定义普通函数是不可能的:
不,在分配的对象上调用free无法恢复内存; free / malloc不需要在使用new分配的东西上工作,如果不调用析构函数,私有数据成员将不会被释放,从而导致内存泄漏。
此外,如果您的程序依赖于它所施加的副作用,忘记调用函数将不会导致未定义的行为;这些副作用根本不会被施加,你的程序将无法满足这些限制,并且可能无法按预期工作。但是,忘记调用析构函数会导致未定义的行为,如第66页所述:
对于类类型的对象 使用非平凡的析构函数,程序不需要在存储之前显式调用析构函数 对象占用的对象被重用或释放;但是,如果没有显式调用析构函数或者如果a delete-expression(5.3.5)不用于释放存储,析构函数不应该被隐式调用 任何取决于析构函数产生的副作用的程序都有未定义的行为。
正如您在原始问题中引用的那样。我不明白你为什么要问这个问题,因为你已经引用了它,但是你去了。
答案 4 :(得分:2)
在评论中你留下了一个简单的问题让我重新思考我说的话。我已经删除了旧的答案,因为即使它有一些价值,也远远不够。
所以你说我的代码是明确定义的,因为即使我打印它也不会依赖于它#34;&#34 ;?这里没有未定义的行为?
让我再说一次,我没有准确记住放置新操作符和释放规则的定义。实际上,我甚至没有全面阅读最新的C ++标准。但如果你引用的文字来自那里,那么你就是在击中UB。
不是由于兰德或打印。或者我们所看到的任何事情&#34;。
此处出现的任何UB都是因为您的代码假定您可以安全地覆盖&#34;一个古老的对象&#39;没有破坏那个坐在那个地方的实例。析构函数的核心副作用不是释放句柄/资源&#34; (您可以在代码中手动执行!)但是留出空间&#34;准备好被回收/重复使用&#34;。
您已假设内存块的使用和对象的生命周期未得到很好的跟踪。我非常确定C ++标准没有定义他们未跟踪。
例如,假设您使用的代码与提供的代码相同,但此结构/类具有vtable
。想象一下,你正在使用超级挑剔的编译器,它有大量的调试工具,可以特别小心地管理vtable,并分配一些额外的bitflag,并将代码注入基础构造函数和析构函数,以翻转该标志以帮助跟踪错误。在这样的编译器上,由于第一个对象的生命周期尚未终止,因此该代码将在new (r) MakeRandom
行崩溃。而且我非常确定这样挑剔的编译器仍然完全符合C ++标准,正如你的编译器肯定也是如此。
这是一个UB。只有大多数编制者都不会做这样的检查。
答案 5 :(得分:2)
首先,我们需要定义未定义的行为,根据C FAQ将在以下时间:
换句话说,这意味着程序员无法预测程序执行后会发生什么。这并不意味着程序或操作系统会崩溃,这很简单意味着程序未来状态只有在执行时才会知道。任何事情都可能发生;标准没有要求。该 程序可能无法编译,或者可能执行不正确(或者 崩溃或静默生成不正确的结果),或者它可能 偶然地做了程序员想要的。
因此,在数学符号中解释,如果程序被简化为函数 F ,它会从初始状态是转换为最终状态 Fs ,给定某些初始条件 Ic
F(Is,Ic) - &gt; FS
如果你评估函数(执行程序) n 次,那么 n-&gt; ∞
F(Is,Ic) - &gt; Fs1 , F(Is,Ic) - &gt; Fs2 ,..., F(Is,Ic) - &gt; Fsn , n-&gt; ∞
然后:
请注意我如何突出显示可能性,因为未定义的行为就是这样。程序有可能按照需要执行,但没有任何保证它会这样做,或者它不会这样做。
因此,回答你的回答:
忘记召唤析构函数,而不是忘记 用同一个身体调用普通函数?
假设析构函数是一个可以被调用的函数,即使你没有显式调用它,忘记调用析构函数也不会忘记调用 普通的功能,这样做可能导致未定义的行为。
理由是,当你忘记打电话给一个普通的功能时,你提前确定,即使你运行你的程序,也不会在你的程序中的任何一点调用该函数。无限次。
然而,当你忘记调用析构函数时,你无限次地调用你的程序,并且如本文所示: https://stackoverflow.com/questions/3179494/under-what-circumstances-are-c-destructors-not-going-to-be-called 在某些情况下,C ++析构函数不会被调用,这意味着您无法事先确定何时调用析构函数,也不会调用析构函数。这种不确定性意味着你不能保证相同的最终状态,从而导致UB。
回答你的第二个问题:
在哪个条件(如果有)下,此程序是否显示未定义 行为?
在我引用的链接上给出了没有调用C ++析构函数的情况下会给出这种情况。
答案 6 :(得分:1)
我对这部分标准的解读是:
这里的副作用只是调用析构函数导致的程序状态的变化。它们会更新引用计数,释放锁定,关闭句柄等等。
'取决于副作用'意味着程序的另一部分需要正确维护引用计数,释放锁,处理关闭等等。如果你练习不调用析构函数,你需要确保你的程序逻辑不依赖于它们被调用。
虽然'遗忘'并不真正相关,但答案是否定的,析构函数只是函数。关键的区别在于,在某些情况下,它们会被编译器调用('隐式'),标准的这一部分定义了它们不会的情况。
你的例子并不真正“依赖于副作用”。它显然将随机函数调用了3次并打印出它计算的任何值。你可以改变它:
显然,使用此依赖关系,程序将针对引用计数显示“未定义的行为”。
请注意,“未定义的行为”不一定是不良行为。它仅仅意味着“本国际标准没有要求的行为”。
我真的认为存在过度思考基本上非常简单的概念的危险。除了这里的文字和标准本身之外,我不能引用任何权威,我觉得很清楚(但无论如何都要告诉我,我是否遗漏了什么)。
答案 7 :(得分:1)
程序“取决于析构函数产生的副作用”取决于“可观察行为”的定义。
引用standard(第1.9.8节,程序执行,添加粗体):
符合实施的最低要求是:
- 严格按照抽象机的规则评估对volatile对象的访问。
- 在程序终止时,写入文件的所有数据应与执行程序的可能结果之一相同 根据抽象语义会产生。
- 交互式设备的输入和输出动态应以促使输出实际传递的方式进行 在程序等待输入之前。什么构成互动 设备是实现定义的。
这些统称为可观察行为 程序。 [注:抽象和抽象之间更严格的对应关系 实际语义可以由每个实现定义。 ]
至于你的另一个问题:
忘记召唤析构函数,而不是忘记 用同一个身体调用普通函数?
是的!忘记对函数的“等效”调用会导致明确定义的行为(无论它应该发生什么都不会发生),但对于析构函数来说却是完全不同的。本质上,标准是说如果你设计程序使得可观察的析构函数“被遗忘”,那么你就不再编写C ++了,你的程序结果是完全未定义的。
修改强> 哦,对,最后一个问题:
在哪个条件(如果有)下,此程序是否显示未定义 行为?
我认为printf有资格写入文件,因此是可观察的。当然rand()实际上并不是随机的,但对于任何给定的种子来说都是完全确定的,所以编写的程序确实表现出未定义的行为(也就是说,如果它没有完全按照写入的方式操作,我会感到非常惊讶,它只是没有't to。)
答案 8 :(得分:0)
假设您有一个类在其构造函数中获取锁,然后在其析构函数中释放锁。释放锁是调用析构函数的一个副作用。
现在,确保调用析构函数是你的工作。通常这是通过调用delete
完成的,但您也可以直接调用它,如果您使用placement new分配了一个对象,通常会这样做。
在您的示例中,您已经分配了2个MakeRandom
个实例,但只在其中一个实例上调用了析构函数。如果它正在管理某些资源(如文件),那么你就会有资源泄漏。
所以,回答你的问题,是的,忘记调用析构函数与忘记调用普通函数不同。析构函数是构造函数的反转。你需要调用构造函数,因此你需要调用析构函数来“解开”析构函数所做的任何事情。 “普通”功能不是这种情况。
答案 9 :(得分:0)
我没有读过其他人的意见,但我有一个简单的解释。在报价中
但是,如果没有显式调用析构函数或者如果a delete-expression不用于释放存储,析构函数 不得暗中调用任何依赖于的程序 析构函数产生的副作用具有未定义的行为。
根据您解析它的方式,含义会有很大差异。 我听到人们在谈论这个意思。
但是,{如果没有显式调用析构函数或者如果a delete-expression不用于释放存储},析构函数 不得暗中调用任何依赖于的程序 析构函数产生的副作用具有未定义的行为。
但我觉得这个意思更有意义
但是,如果没有显式调用析构函数或{如果a delete-expression不用于释放存储,析构函数 不得暗中调用任何依赖于的程序 析构函数产生的副作用具有未定义的行为}。
基本上说C ++没有垃圾收集器,如果你假设的话 它确实有GC,你的程序将无法按预期工作。
答案 10 :(得分:0)
它基本上意味着当您为类定义自己的析构函数时,在离开作用域时不再自动调用它。如果您尝试使用它,该对象仍将超出范围,但内存仍将在堆栈中用完,并且非默认析构函数中的任何内容都不会发生。例如,如果您希望在调用析构函数时减少对象的数量,则不会发生这种情况。
答案 11 :(得分:0)
要求标准以observable behavior
和side effects
这样的术语发言,因为尽管许多人经常忘记这一点,但c ++不仅仅用于PC软件。
考虑一下你对Gene的回答的评论中的例子:
class S {
unsigned char x;
public: ~S() {
++x;
}
};
例如,你错过了嵌入式世界。考虑在小型处理器上运行的裸机c ++程序,该程序具有对uart的特殊功能寄存器访问:这里的析构函数显然正在修改一个对象 - 因此这是一个 具有给定定义的“副作用” - 但我很确定没有 程序可以在任何合理的意义上“依赖”这种副作用 术语。我错过了什么?
new (address_of_uart_tx_special_function_register) S;
这里调用析构函数显然有可观察到的副作用。如果我们不调用它,UART会减少一个字节。
因此,是否可观察到副作用还取决于硬件对写入某些存储器位置的操作。
值得注意的是,即使析构函数的主体是空的,如果任何类成员变量具有副作用的析构函数,它仍然可能具有副作用。
我没有看到任何禁止编译器执行其他簿记的事情(可能是关于异常和堆栈展开)。即使目前没有编译器,也没有编译器从语言律师的角度来看,你仍然必须考虑UB,除非你知道编译器不会产生副作用。
答案 12 :(得分:0)
措辞的这个确切问题是针对C ++标准草案[basic.life] Remove description of impossible UB提出的编辑要求的主题,该标准试图从标准草案中删除该措辞:
未被调用的析构函数不会产生副作用,因此不可能依赖那些副作用。
经过大量讨论,它似乎倾向于该方向:
编辑会议:标准规则不能取决于程序员的意图。打算照原样传递给CWG。
,但首先需要由核心工作组(CWG)进行审核,因此这不是编辑更改。我相信这意味着它最终会显示在as a defect report上。
因此,总之,这似乎是一个尚待解决的问题,有关该措词是否具有任何意义,但最终将由CWG进行审查。