【问题标题】:Is delete allowed to modify its parameter?是否允许删除修改其参数?
【发布时间】:2017-12-22 07:36:03
【问题描述】:

https://stackoverflow.com/a/704568/8157187 的回答中,引用了 Stroustrup 的一句话:

C++ 明确允许删除的实现将 左值操作数,我曾希望实现能做到这一点, 但这个想法似乎并没有受到实施者的欢迎。

但是,我没有在标准中找到这个明确的声明。当前标准草案 (N4659) 的一部分可以这样解释:

6.7:

当一个存储区域的持续时间结束时, 表示任何部分地址的所有指针的值 存储区域变为无效的指针值 (6.9.2)。间接的 通过无效的指针值并传递无效的指针值 释放函数具有未定义的行为。任何其他使用 无效的指针值具有实现定义的行为。

脚注:某些实现可能会定义复制无效指针值会导致系统生成的运行时错误

所以,在delete ptr; 之后,ptr 的值变成了一个无效的指针值,并且使用这个值具有实现定义的行为。但是,并没有说ptr的值是允许改变的。

这可能是一个哲学问题,如果一个人不能使用它的值,怎么能确定一个值发生了变化?

6.9:

对于平凡的任何对象(基类子对象除外) 可复制类型 T,无论对象是否拥有类型的有效值 T,构成对象的底层字节(4.4)可以复制到 char、unsigned char 或 std::byte (21.2.1) 的数组。43 如果 该数组的内容被复制回对象中,该对象应 随后保持其原始值。

所以,看来,memcpy 将无效指针值放入 char 数组是有效的(取决于哪个语句“更强”,6.7 或 6.9。对我来说,6.9 似乎更强)。

这样,我可以检测到指针值已被delete 改变:memcpy 指针在delete 前后的值与char 数组相比,然后比较它们。

因此,据我了解,6.7 不允许 delete 修改其参数。

delete是否允许修改其参数?

在此处查看 cmets: https://stackoverflow.com/a/45142972/8157187


这是一个不太可能但仍然可能的实际代码,这很重要:

SomeObject *o = ...; // We have a SomeObject
// This SomeObject is registered into someHashtable, with its memory address
// The hashtable interface is C-like, it handles opaque keys (variable length unsigned char arrays)

delete o;

unsigned char key[sizeof(o)];
memcpy(key, &o, sizeof(o)); // Is this line OK? Is its behavior implementation defined?
someHashtable.remove(key, sizeof(key)); // Remove o from the hashtable

当然,这个 sn-p 可以重新排序,所以它成为一个肯定有效的代码。但问题是:这是一个有效的代码吗?


这是一个相关的思路:假设一个实现确实定义了脚注描述的内容:

复制无效的指针值会导致系统生成的运行时错误

6.9 保证我可以memcpy() 任何值。甚至是无效的。所以在这个理论实现中,当我memcpy() 无效指针值(应该成功,6.9 保证)时,从某种意义上说,我不使用无效指针值,而只使用它的底层字节(因为它会生成一个运行时错误,而 6.9 不允许),因此 6.7 不适用

【问题讨论】:

  • "这可能是一个哲学问题,如果一个人不能使用它的价值,如何确定价值已经改变?"那句话上面的段落只是说明你不能取消引用指针,但是像delete p; if (p==someOtherPOinter){}这样的东西应该没问题
  • @geza 由于使用它的结果是实现定义的,所以实现可以定义if (p == nullptr)对于这个无效值是真的。
  • @geza 然后我遵循“好像”规则。如果所有操作的行为都与更改值一样,则允许实现更改它。
  • @geza 什么规则禁止更改 int 的内部位?据我所知,该实现甚至允许int a = 0; /* test bits of 'a' now */; /* test bits of 'a' now*/ 每次有两个不同的位。
  • @geza 是的,正如我在回答中提到的,重要的是价值,价值和表示之间没有 1-1 对应关系(在任一方向上)。通常当然是为了节省内存用法,但一个例子是 1 的补码整数的负零,另一个广泛的情况是类型中未使用的位,我将我们在这个问题上讨论的部分解释为如果指针具有无效值,那么它独立于代表,即无论代表什么。也就是说,它仍然是一个无效值,并且在分配一个有效值之前会一直保持这种状态

标签: c++ language-lawyer c++17


【解决方案1】:

在删除之前,ptr 的值是有效的。删除后,该值无效。因此价值发生了变化。有效值和无效值是互斥的——一个值不能同时有效和​​无效。

你的问题有一个基本的误解;您将这两个不同的概念混为一谈:

  • 变量的值
  • 变量在内存中的表示。

这两件事之间没有一一对应的关系。同一个值可能有多种表示,同一个表示可能对应不同的值。


我认为您问题的要点是:delete ptr; 可以更改 ptr 的表示形式吗?。答案是“是”。您可以将已删除的指针 memcpy 到 char 数组中,检查字节,并发现它们都是零值字节(或其他任何字节)。这在 C++14 [basic.stc.dynamic.deallocation]/4(或 C++17 [basic.stc]/4)的标准中有所涵盖:

对无效指针值的任何其他使用都具有实现定义的行为。

它是实现定义的,实现可以定义检查字节给出值为零的字节。


您的代码 sn-p 依赖于实现定义的行为。 “有效代码”不是标准使用的术语,但代码可能不会从哈希表中删除预期的项目。

正如 Stroustrup 所暗示的,这是一个有意的设计决定。一个示例用法是在调试模式下将已删除指针设置为特定表示的编译器,以便如果随后使用已删除指针,它可以引发运行时错误。 Here's an example 对未初始化指针起作用的原则。

历史记录:在 C++11 中,这种情况是未定义,而不是实现定义的。因此,使用已删除指针的行为与使用未初始化指针的行为相同。在 C 语言中,释放内存被定义为将指向该内存的所有指针置于与未初始化指针相同的状态。

【讨论】:

  • 一个给定的值当然可以并且确实在调用时变得无效。指针变量不必更改其值即可变为无效。此外,您将实现行为与指定的(可能是实现定义的)行为混淆了。
  • @philipxy 我是说从“指针变量具有有效值”到“指针变量具有无效值”重言式意味着值已更改。 (我认为您的误解与我在回答中指出的相同;representation 可能没有改变,但值却改变了)。
  • @philipxy 当释放发生时,所有指向已释放存储的任何部分的指针值都变为无效值,包括存储在变量中的值。参见 [basic.stc.dynamic.deallocation]/4。 delete 的“参数”不仅仅是特定值。
  • “成为”意味着改变。您是否拒绝我的断言,即从有效变为无效是一种变化?如果是这样,那么我们就无法真正进一步交流,因为对我来说,这是“改变”概念的基本属性。 (注意。无论如何,我认为这是一个有争议的问题,因为我相信 OP 真的在询问 ptr 的表示是否可以改变)。
  • 我理解您试图提出的哲学观点(我同意),但措辞真的没有帮助。说“价值已经改变”是非常,非常,令人困惑。如果您的第三句话是“因此值已更改(尽管内存中的位表示可能尚未完成),那将有很大帮助。”或者,将您的第二段移到第一段。
【解决方案2】:

您从 Stroustrup 中找到该声明的上下文可在 Stroustrup, delete zero 下找到

Stroustrup 让你考虑

delete p;
// ...
delete p;

第一次删除后,指针 p 无效。第二次删除是错误的,但是如果第一次删除后 p 设置为 0 就没有效果了。

Stroustrups 的想法是将其编译为类似

delete p; p = 0;
// ...
delete p;

delete 本身无法将指针归零,因为它通过了void * 但不是void *&

但是,我发现将 p 归零并没有多大帮助,因为它们可能存在该指针的其他副本,这些副本也可能被意外删除。 更好的方法是使用不同类型的智能指针。

规范 6.7

表示在删除 p 之后,任何具有指针 p 地址的指针都将无效(以实现定义的方式)。它没有说改变指针地址,既不允许也不允许。

规范 6.9

6.9 的先决条件是一个对象(有效与否)。此规范不适用于此处,因为 p(地址)在删除后不再有效,因此不指向对象。所以没有矛盾,任何讨论6.7还是6.9更强都是无效的。

规范还要求将字节复制回原始对象位置,您的代码不会这样做,也不能这样做,因为原始对象已被删除。


但是,我认为没有理由将指针地址打包到 char 数组中并传递它。并且指针在某个实现中始终具有相同的大小。您的代码只是一个繁琐的版本:

SomeObject *o = ...;

void *orgO = o;
delete o;

someHashtable.remove(orgO);
// someHashtable.remove(o); o might be set to 0

但是,这段代码看起来仍然很奇怪。要从哈希表中获取对象,您需要指向该对象的指针。为什么不直接使用指针呢??

哈希表应该有助于通过对象的一些不变值来查找对象。那不是你的哈希表应用

您是否打算获得SomeObject 的所有有效实例的列表?

您的代码无效原因,根据 Stroustrup 的说法,允许编译器将 p 设置为零。如果发生这种情况,您的代码将崩溃

【讨论】:

  • 我的代码没有取消引用指针。我认为你完全误解了这个主题。这不是访问释放的内存,而是访问指针本身。
  • 好的,我现在看到了
【解决方案3】:

通过无效指针值的间接传递以及将无效指针值传递给释放函数具有未定义的行为。对无效指针值的任何其他使用都具有实现定义的行为。

所以,在delete ptr; 之后,ptr 的值变成了一个无效的指针值,并且使用这个值具有实现定义的行为。

标准是说传递的指针值“变得无效”,即它的状态已经改变,使得某些调用变得未定义,实现可以不同地对待它。

语言不是很清楚,但这里是上下文:

6.7 存储期限
4 当一个存储区域的持续时间结束时,代表该存储区域任何部分地址的所有指针的值将变为无效指针值(6.9.2)。通过无效指针值的间接传递以及将无效指针值传递给释放函数具有未定义的行为。对无效指针值的任何其他使用都具有实现定义的行为。

6.9.2 复合类型
指针类型的每个值都是以下之一:
(3.1) — 指向对象或函数的指针(指针被称为指向对象或函数),或
(3.2) — 超过对象末尾的指针 (8.7),或
(3.3) — 该类型的空指针值 (7.11),或
(3.4) — 无效的指针值。

指针类型的值是无效的还是不无效的,它们根据程序在C++抽象机上的执行进度“变成”。

该标准并未讨论左值寻址的变量/对象所持有的值的更改或符号与值的关联的更改。

C++ 明确允许删除的实现将 左值操作数,我曾希望实现能做到这一点, 但这个想法似乎并没有受到实施者的欢迎。

与此分开,Stroustrup 是说,如果操作数表达式是可修改的左值,即操作数表达式是持有被传递的指针值的变量/对象的地址,之后该值的状态为“无效”,则实现可以将该变量/对象持有的值设置为零。

但是,并没有说ptr的值是允许改变的。

Stroustrup 通过谈论实现可以做什么是非正式的。该标准定义了抽象 C++ 机器可以/不能/可能的行为方式。在这里,Stroustrup 正在谈论一个看起来像那台机器的假设实现。 “ptr 的值”是“允许更改的”,因为定义和未定义的行为不会让您知道释放时的值是什么,并且实现定义的行为可以是任何东西,所以它可以是变量/对象持有不同的值。

谈论价值改变是没有意义的。您不能将值“归零”;您可以将变量/对象清零,这就是我们所说的“清零”左值的意思——将它引用/标识的变量/对象清零。即使您将“归零”扩展为包括将新值与名称或文字相关联,实现也可以这样做,因为您无法在运行时通过名称或文字“使用”该值来确定它是否仍与相同的值。

(然而,因为一个值可以做的就是在程序中“使用”它,方法是传递一个左值来标识一个变量/对象,将其传递给一个运算符,或者传递一个表示它的引用或常量给一个运算符,并且操作员可以表现得好像传递了一个 不同的 值,我想你可以合理地非正式地草率地将其捕获为实现中的“值改变值”。)

如果该数组的内容被复制回对象中,则该对象随后应保持其原始值。

但是复制它正在使用它,因此一旦它“无效”,复制它就是实现定义的。因此,调用通常会复制它的程序是实现定义的。脚注清楚地说明了这一点,该脚注给出了

某些实现可能会定义复制无效指针值会导致系统生成的运行时错误

对于未定义/实现定义的行为,它通常不会做任何事情。我们使用正常行为来确定对抽象机器的一系列更改,如果出现实现定义的状态更改,那么事情的行为就像实现定义了它们的行为,而不是它们通常的行为方式。遗憾的是,“使用”一个值的含义并不清楚。我不知道为什么你认为 6.9 “保证”比任何东西都或多或少地记忆,在未定义/实现定义的状态之后什么都不是。

【讨论】:

  • 对我来说,这些并不是那么明显。该脚注(无效的指针值导致运行时错误)与 6.9 冲突,这保证了即使是无效值也可以被复制。你如何解决这个问题?
  • 6.9 明确提到了无效值:“对象是否持有类型 T 的 有效 值”。所以似乎 memcpy 不被认为是“使用”。或者至少这是我能给出的唯一解释。至少可以这样解决冲突。
  • 我同意您的观点,即“变为无效”并不一定意味着该值已更改。但接受的答案的其他部分似乎适合我。
  • 不管 6.9 的问题如何,Stroutrup 都在谈论将变量/对象归零。另外,我无法从您的问题中看出您认为什么是“归零”。
  • 我认为他谈到了将指针本身归零(或者您的评论是什么意思?),这就是左值要求的原因。
【解决方案4】:

为了让删除函数更新指针,它需要知道这个指针的地址并进行更新。这将需要额外的内存、少量的额外操作和编译器支持。在您的示例中看起来或多或少是微不足道的。

现在想象一个函数链,它们在参数中相互传递指针,只有最后一个会真正删除。在这种情况下要更新哪些指针?最后一个?全部?对于后者,需要创建一个动态的指针列表:

Objec *o = ...
handle(o);
void handle(Object *o){
   if (deleteIt) doDelete(0);
   else doSomethingElseAndThenPossiblyDeleteIt(o);
}
void doDelete(Object *o) {
    delete o;
}

因此,从哲学上讲,如果允许删除修改其参数,它将打开一个降低程序效率的热罐。所以,这是不允许的,我希望永远不会。在这些情况下,未定义的行为可能是最自然的事情。

至于内存内容,不幸的是,我看到太多错误,在删除指针后,已删除的内存被覆盖。而且......它工作正常,直到某个时刻到来。由于内存被标记为空闲,它最终会被其他对象重用,结果非常无趣并且需要大量调试。因此,再次从哲学上讲,c++ 不是一种易于编程的语言。在没有任何语言支持的情况下,还有其他工具可以解决这些问题。

【讨论】:

  • 它不需要必须修改参数,它只是一个选项。 IMO 在调试模式下使用是很自然的。
  • @M.M 有一些调试和 linting 工具可以在没有这个的情况下捕获这些问题。
  • @Serge:你从实践的角度回答了这个问题。但是这个问题是关于标准的。当您说“所以,这是不允许的,我希望永远不会”时,这是出于效率原因的支持。但我想引用标准中的内容以及对它们的解释。
  • @geza 我想我的意思是我命令 top 允许 'dlelete' 修改其指针,标准必须提供将其传递给函数的机制以及处理链分配的方法和其他情况。它绝对不会这样做。
猜你喜欢
  • 1970-01-01
  • 2015-12-22
  • 2012-04-18
  • 2011-12-11
  • 1970-01-01
  • 1970-01-01
  • 2014-01-31
  • 2011-09-17
  • 1970-01-01
相关资源
最近更新 更多