除了调用全局删除运算符之外,删除一个void指针还能做什么呢

How can deleting a void pointer do anything other than invoke the global delete operator?

本文关键字:删除 指针 void 一个 什么 全局 调用 运算符      更新时间:2023-10-16

C++标准非常明确地指出,在void指针上使用deletedelete[]是未定义的行为,如以下答案所引用:

这意味着不能使用类型为void*的指针删除对象,因为没有类型为void的对象。

然而,据我所知,deletedelete[]只做两件事:

  • 调用适当的析构函数
  • 调用适当的operator delete函数,通常是全局函数

有一个单独的参数operator delete(以及operator delete[]),该单独的参数是void* ptr

因此,当编译器遇到带有void*操作数的删除表达式时,当然可能会恶意执行一些完全无关的操作,或者只是不输出该表达式的代码。更好的是,它可以发出诊断消息并拒绝编译,尽管我测试过的MSVS、Clang和GCC版本没有这样做。(后两者使用-Wall发出警告;使用/W3的MSVS不发出警告。)

但实际上只有一种合理的方法来处理删除操作中的上述步骤:

  • void*没有指定析构函数,因此不会调用析构函数
  • void不是类型,因此不能具有特定的对应operator delete,因此必须调用全局operator delete(或[]版本)。由于函数的参数是void*,因此不需要进行类型转换,并且运算符函数的行为必须正确

那么,当遇到这样的删除表达式时,是否可以依靠常见的编译器实现(可能不是恶意的,否则我们甚至不能相信它们会遵守标准)来执行上述步骤(在不调用析构函数的情况下释放内存)?如果没有,为什么不呢?如果是这样的话,当实际类型的数据没有析构函数(例如,它是一个基元数组,如long[64])时,以这种方式使用delete安全吗?

全局删除运算符void operator delete(void* ptr)(以及相应的数组版本)是否可以安全地直接为void*数据调用(再次假设不应该调用析构函数)?

void*是指向未知类型对象的指针。如果你不知道某种东西的类型,你就不可能知道它是如何被摧毁的。因此,我认为,不,并不是"只有一种合理的方式来处理这样的删除操作"。处理这种删除操作的唯一明智方法是不处理它。因为根本不可能正确处理它。

因此,正如您链接到的原始答案所说:删除void*是未定义的行为([expr.delete]§2)。该答复中提到的脚注至今基本保持不变。老实说,我有点惊讶,这只是被指定为未定义的行为,而不是使其格式错误,因为我想不出在编译时无法检测到这种情况。

请注意,从C++14开始,new表达式并不一定意味着对分配函数的调用。delete表达式也不一定意味着对释放函数的调用。编译器可以调用一个分配函数来获得用new表达式创建的对象的存储。在某些情况下,编译器可以省略这样的调用,并使用以其他方式分配的存储。例如,这使得编译器有时能够将使用new创建的多个对象打包到一个分配中。

void*上调用全局解除分配函数而不是使用delete表达式是否安全?仅当使用相应的全局分配功能分配存储时。通常,除非您自己调用了分配函数,否则您无法确定这一点。如果您从new表达式中获得指针,您通常不知道该指针是否是解除分配函数的有效参数,因为它甚至可能不指向通过调用分配函数获得的存储。注意,知道new表达式必须使用哪个分配函数基本上相当于知道void*所指向的动态类型。如果你知道这一点,你也可以只将static_cast<>转换为实际类型,delete转换为…

在不首先显式调用析构函数的情况下,用平凡的析构函数释放对象的存储是否安全?根据[基本生活]§1.4,我会同意。请注意,如果该对象是一个数组,您可能仍然需要首先调用任何数组元素的析构函数。除非它们也是琐碎的。

你能依靠通用的编译器实现来产生你认为合理的行为吗?不。对你可以依赖的东西有一个正式的定义,这就是制定标准的初衷。假设你有一个符合标准的实施,你可以依靠标准给你的保证。您也可以依赖特定编译器的文档可能给您的任何额外保证,只要您使用该特定编译器的特定版本来编译代码。除此之外,所有的赌注都被取消了…

如果您想调用deallocation函数,那么只需调用deallolocation函数即可。

这很好:

void* p = ::operator new(size);
::operator delete(p);  // only requires that p was returned by ::operator new()

这不是:

void* p = new long(42);
delete p;  // forbidden: static and dynamic type of *p do not match, and static type is not polymorphic

但请注意,这也不安全:

void* p = new long[42];
::operator delete(p); // p was not obtained from allocator ::operator new()

虽然标准允许实现使用传递给delete的类型来决定如何清理有问题的对象,它不需要实现。该标准还允许一种替代方法(可以说是优越的),该方法基于将分配new的内存存储在返回地址之前的空间中,并将delete实现为对以下内容的调用:

typedef void(*__cleanup_function)(void*);
void __delete(void*p)
{
*(((__cleanup_function*)p)[-1])(p);
}

在大多数情况下,以这种方式实现new/delete的成本相对较小,并且该方法将提供一些语义优势。这种方法唯一显著的缺点是,它将要求记录其new/delete实现的内部工作的实现,以及其实现不能支持类型不可知的delete的实现,必须破坏任何依赖于其记录内部工作的代码。

请注意,如果将void*传递给delete是一种约束冲突,则这将禁止实现提供类型不可知的delete,即使它们很容易做到这一点,即使为它们编写的某些代码依赖于这种能力。当然,代码依赖于这样的能力这一事实将使其仅可移植到能够提供这种能力的实现,但如果实现选择支持这种能力,则允许它们支持这种能力比使其成为约束冲突更有用。

就我个人而言,我希望看到标准提供两种具体的选择:

  1. 允许向delete传递void*,并使用传递给new的任何类型删除对象,并定义一个指示支持此类构造的宏。

  2. 如果void*被传递给delete,则发出诊断,并定义一个宏,指示它不支持这样的构造。

实现支持类型不可知delete的程序员可以决定他们从这种功能中获得的好处是否能够证明使用它所带来的可移植性限制,实现人员可以决定支持更广泛程序的好处是否足以证明支持该功能的小成本是合理的。

void*没有指定析构函数,因此不会调用析构函数。

这很可能是不允许的原因之一。在不调用类的析构函数的情况下,取消分配支持类实例的内存是一个非常非常糟糕的主意。

例如,假设该类包含一个std::map,其中有几十万个元素。这表示相当大的内存量。做你的提议会泄露所有的记忆。

void没有大小,因此编译器无法知道要释放多少内存。

编译器应该如何处理以下内容?

struct s
{
int arr[100];
};
void* p1 = new int;
void* p2 = new s;
delete p1;
delete p2;