是否允许删除修改其参数?

时间:2017-07-17 16:57:05

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

在答案https://stackoverflow.com/a/704568/8157187中,引用了Stroustrup:

  

C ++明确允许将delete的实现归零   左值操作数,我曾希望实现会这样做,   但是这个想法似乎并没有受到实施者的欢迎。

但是,我没有在标准中找到这个明确的陈述。目前的标准草案(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允许修改其参数的权利。

是否允许删除修改其参数?

点击此处的评论: 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

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

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

  

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

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

5 个答案:

答案 0 :(得分:12)

删除前,ptr的值有效。删除后,该值无效。因此价值改变了。有效值和无效值是互斥的 - 值不能同时有效且无效。

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

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

这两件事之间没有一对一的对应关系。相同的值可以具有多个表示,并且相同的表示可以对应于不同的值。

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

  

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

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

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

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

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

答案 1 :(得分:2)

delete在[expr.delete]中定义,用于调用释放函数,释放函数在[basic.stc.dynamic.deallocation]中定义为:

  

每个释放函数都应返回void,其第一个参数应为void*

由于所有释放函数都是void*,而不是void*&,因此没有机制可以让他们修改参数。

答案 2 :(得分:2)

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

下找到

Stroustrup让你考虑

delete p;
// ...
delete p;

第一次删除后,指针p无效。第二次删除是错误的,但如果在第一次删除后将p设置为0,则无效。

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

的东西
delete p; p = 0;
// ...
delete p;

删除自身无法将指针归零,因为它通过了void *而不是void *&

然而,我发现零输出并不是很有帮助,因为它们可能存在可能意外删除的指针的其他副本。 更好的方法是使用不同类型的智能指针。

规格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 :(得分:2)

  
    

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

  

  

因此,在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 ++明确允许将delete的实现归零     左值操作数,我曾希望实现会这样做,     但是这个想法似乎并没有受到实施者的欢迎。

  

单独,Stroustrup说如果操作数表达式是一个可修改的左值,即操作数表达式是一个变量/对象的地址,其中包含一个传递的指针值,之后该值的状态为"无效",然后实现可以将该变量/对象持有的值设置为零。

  

但是,它并没有说ptr的值可以更改。

通过谈论实现可以做什么,Stroustrup是非正式的。该标准定义了抽象C ++机器如何/不能/可能的行为。在这里,Stroustrup正在谈论一个看起来像那台机器的假设实现。 A" ptr的价值"被允许改变"因为已定义和未定义的行为不会让您找出该值与释放相关的内容,并且实现定义的行为可以是任何内容,因此可以是变量/对象保存不同的值。

谈论价值变化是没有意义的。你不能零"一个值;你可以将一个变量/对象归零,这就是我们说"归零"左值 - 将其引用/标识的变量/对象清零。即使你伸展"归零"要包括将新值与名称或文字相关联,实现可以执行此操作,因为您不能使用"运行时通过名称或文字的值,以确定它是否仍然与相同的值相关联。

(但是,因为所有人都可以使用值#34;使用&#34;它在程序中通过传递一个左值来识别保存它的变量/对象给操作符,或者传递一个引用或常量来表示它对于一个操作员,一个操作员可以表现得好像一个不同的值被传递,我猜你可以合理地非正式地抓住那个&#34;值改变值&#34;在实现中。)< / p>

  
    

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

  

但复制它正在使用它,因此复制它是实现定义的,一旦它是&#34;无效&#34;。因此,调用通常会复制它的程序是实现定义的。通过提供

示例的脚注清楚地表明了这一点
  
    

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

  

对于未定义/实现定义的行为,它通常没有做什么。我们使用正常行为来确定抽象机器的一系列更改,如果实现了一个实现定义的状态更改,那么事情的行为就像实现将它们定义为行为,而不是它们通常的行为。可悲的是&#34;使用&#34;价值不明确。我不知道你为什么认为6.9&#34;保证&#34;任何事情或多或少地重新记忆任何东西,在未定义/实现定义的状态之后什么都不是。

答案 4 :(得分:1)

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

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

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

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

至于内存内容,遗憾的是我看到太多错误,删除指针后删除的内存会被覆盖。并且...它一直工作,直到片刻。由于内存被标记为空闲,因此最终会被其他对象重用,并且会产生非常无趣的后果和大量的调试。所以,再次哲学地说,c ++并不是一种易于编程的语言。没有任何语言支持,还有其他工具可以捕获这些问题。