删除 void 指针除了调用全局删除运算符之外还能做些什么?
How can deleting a void pointer do anything other than invoke the global delete operator?
C++ 标准非常清楚明确地指出,在 void
指针上使用 delete
或 delete[]
是未定义的行为,如 this answer 中引用:
This implies that an object cannot be deleted using a pointer of type void*
because there are no objects of type void
.
然而,据我了解,delete
和 delete[]
只做两件事:
- 调用适当的析构函数
- 调用适当的
operator delete
函数,通常是全局函数
有一个单参数operator delete
(以及operator delete[]
),和that single argument is void* ptr
.
因此,当编译器遇到带有 void*
操作数的删除表达式时,它当然 可以 maliciously 执行一些完全不相关的操作,或者只是不输出该表达式的代码。更好的是,它可以发出诊断消息并拒绝编译,尽管我测试过的 MSVS、Clang 和 GCC 版本不会这样做。 (后两者使用 -Wall
发出警告;使用 /W3
的 MSVS 则不会。)
但是实际上只有一种明智的方法可以处理删除操作中的上述每个步骤:
void*
指定没有析构函数,因此不会调用任何析构函数。
void
不是类型,因此不能有特定对应的 operator delete
,因此必须调用全局 operator delete
(或 []
版本)。由于函数的参数是 void*
,因此不需要类型转换,并且运算符函数的行为必须正确。
那么,是否可以依靠常见的编译器实现(大概不是恶意的,否则我们甚至不能相信它们无论如何都遵守标准)遵循上述步骤(释放内存而不调用析构函数)当遇到这样的删除表达式?如果不是,为什么不呢?如果是这样,当数据的实际类型没有析构函数时,以这种方式使用 delete
是否安全(例如,它是一个基元数组,如 long[64]
)?
是否可以直接为 void*
数据安全地调用全局删除运算符 void operator delete(void* ptr)
(以及相应的数组版本)(再次假设不应调用析构函数)?
如果要调用释放函数,直接调用释放函数即可。
这个不错:
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()
void* specifies no destructor, so no destructors are invoked.
这很可能是不允许的原因之一。在不为所述 class 调用析构函数的情况下释放支持 class 实例的内存是一个非常糟糕的主意。
假设,例如,class 包含一个 std::map
,其中有几十万个元素。这代表了大量的内存。执行您提议的操作会泄漏所有内存。
A void*
是指向未知类型对象的指针。如果你不知道某物的类型,你就不可能知道如何销毁它。所以我认为,不,不存在 "really only one sensible way to deal with such a delete operation"。处理此类删除操作的唯一明智方法是不处理它。因为根本没有办法正确处理它。
因此,正如您链接到的原始答案所说:删除 void*
是未定义的行为([expr.delete] §2). The footnote mentioned in that answer remains essentially unchanged to this day。老实说,我有点惊讶,这只是被指定为未定义的行为,而不是使其格式错误,因为我想不出在编译时无法检测到的任何情况。
请注意,从 C++14 开始,new
表达式不一定意味着调用分配函数。 delete
表达式也不一定暗示调用释放函数。编译器may call an allocation function to obtain storage for an object created with a new
expression. In some cases,允许编译器省略这样的调用并使用以其他方式分配的存储。例如,这使编译器有时可以将使用 new
创建的多个对象打包到一个分配中。
在 void*
上调用全局释放函数而不是使用 delete
表达式是否安全?仅当存储分配有相应的全局分配函数时。通常,除非您自己调用分配函数,否则您无法确定。如果您从 new
表达式获得指针,您通常不知道该指针是否甚至是释放函数的有效参数,因为它甚至可能不指向通过调用分配函数获得的存储。请注意,知道 new
表达式必须使用哪个分配函数基本上等同于知道 void*
指向的任何内容的动态类型。如果你知道这一点,你也可以 static_cast<>
到实际类型和 delete
它...
在不首先显式调用析构函数的情况下,使用平凡的析构函数释放对象的存储是否安全?基于 [basic.life] §1.4,我会说是。请注意,如果该对象是一个数组,您可能仍然必须先调用任何数组元素的析构函数。除非它们也是微不足道的。
您能否依靠常见的编译器实现来产生您认为合理的行为?不。对您 可以 所依赖的东西有一个正式的定义,这实际上是首先拥有一个标准的全部意义所在。假设您有一个符合标准的实现,您可以依赖标准为您提供的保证。您还可以依赖特定编译器的文档可能为您提供的任何额外保证,只要您使用该特定编译器的特定版本来编译您的代码。除此之外,所有赌注都取消了……
A void
没有大小,因此编译器无法知道要释放多少内存。
编译器应如何处理以下内容?
struct s
{
int arr[100];
};
void* p1 = new int;
void* p2 = new s;
delete p1;
delete p2;
虽然标准允许实现使用传递给 delete
的类型来决定如何清理有问题的对象,但它并不要求实现这样做。该标准还将允许一种替代(并且可以说是更好的)方法,该方法基于让内存分配 new
将清理信息存储在紧接返回地址之前的 space 中,并将 delete
实现为调用类似:
typedef void(*__cleanup_function)(void*);
void __delete(void*p)
{
*(((__cleanup_function*)p)[-1])(p);
}
在大多数情况下,以这种方式实现 new
/delete
的成本相对微不足道,而且该方法会带来一些语义上的好处。这种方法的唯一重大缺点是它需要记录其 new
/delete
实现的内部工作的实现,并且其实现不能支持类型不可知的 delete
, 将不得不破坏任何依赖于他们记录的内部工作原理的代码。
请注意,如果将 void*
传递给 delete
是违反约束的,这将禁止实现提供与类型无关的 delete
,即使它们很容易做到所以,即使为他们编写的一些代码会依赖于这种能力。代码依赖于这种能力的事实使其只能移植到可以提供它的实现,当然,但是允许实现支持这种能力(如果他们选择这样做)比使其违反约束更有用。
就个人而言,我希望看到标准提供实现两个特定的选择:
允许将 void*
传递给 delete
并使用传递给 new
的任何类型删除该对象,并定义一个宏来指示支持此类构造.
如果 void*
传递给 delete
,则发出诊断,并定义一个宏指示它不支持这样的构造。
实现支持类型不可知的程序员 delete
然后可以决定他们可以从这种特性中获得的好处是否可以证明使用它所施加的可移植性限制是合理的,实现者可以决定支持更广泛的特性的好处是否合理程序的范围足以证明支持该功能的小成本。
C++ 标准非常清楚明确地指出,在 void
指针上使用 delete
或 delete[]
是未定义的行为,如 this answer 中引用:
This implies that an object cannot be deleted using a pointer of type
void*
because there are no objects of typevoid
.
然而,据我了解,delete
和 delete[]
只做两件事:
- 调用适当的析构函数
- 调用适当的
operator delete
函数,通常是全局函数
有一个单参数operator delete
(以及operator delete[]
),和that single argument is void* ptr
.
因此,当编译器遇到带有 void*
操作数的删除表达式时,它当然 可以 maliciously 执行一些完全不相关的操作,或者只是不输出该表达式的代码。更好的是,它可以发出诊断消息并拒绝编译,尽管我测试过的 MSVS、Clang 和 GCC 版本不会这样做。 (后两者使用 -Wall
发出警告;使用 /W3
的 MSVS 则不会。)
但是实际上只有一种明智的方法可以处理删除操作中的上述每个步骤:
void*
指定没有析构函数,因此不会调用任何析构函数。void
不是类型,因此不能有特定对应的operator delete
,因此必须调用全局operator delete
(或[]
版本)。由于函数的参数是void*
,因此不需要类型转换,并且运算符函数的行为必须正确。
那么,是否可以依靠常见的编译器实现(大概不是恶意的,否则我们甚至不能相信它们无论如何都遵守标准)遵循上述步骤(释放内存而不调用析构函数)当遇到这样的删除表达式?如果不是,为什么不呢?如果是这样,当数据的实际类型没有析构函数时,以这种方式使用 delete
是否安全(例如,它是一个基元数组,如 long[64]
)?
是否可以直接为 void*
数据安全地调用全局删除运算符 void operator delete(void* ptr)
(以及相应的数组版本)(再次假设不应调用析构函数)?
如果要调用释放函数,直接调用释放函数即可。
这个不错:
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()
void* specifies no destructor, so no destructors are invoked.
这很可能是不允许的原因之一。在不为所述 class 调用析构函数的情况下释放支持 class 实例的内存是一个非常糟糕的主意。
假设,例如,class 包含一个 std::map
,其中有几十万个元素。这代表了大量的内存。执行您提议的操作会泄漏所有内存。
A void*
是指向未知类型对象的指针。如果你不知道某物的类型,你就不可能知道如何销毁它。所以我认为,不,不存在 "really only one sensible way to deal with such a delete operation"。处理此类删除操作的唯一明智方法是不处理它。因为根本没有办法正确处理它。
因此,正如您链接到的原始答案所说:删除 void*
是未定义的行为([expr.delete] §2). The footnote mentioned in that answer remains essentially unchanged to this day。老实说,我有点惊讶,这只是被指定为未定义的行为,而不是使其格式错误,因为我想不出在编译时无法检测到的任何情况。
请注意,从 C++14 开始,new
表达式不一定意味着调用分配函数。 delete
表达式也不一定暗示调用释放函数。编译器may call an allocation function to obtain storage for an object created with a new
expression. In some cases,允许编译器省略这样的调用并使用以其他方式分配的存储。例如,这使编译器有时可以将使用 new
创建的多个对象打包到一个分配中。
在 void*
上调用全局释放函数而不是使用 delete
表达式是否安全?仅当存储分配有相应的全局分配函数时。通常,除非您自己调用分配函数,否则您无法确定。如果您从 new
表达式获得指针,您通常不知道该指针是否甚至是释放函数的有效参数,因为它甚至可能不指向通过调用分配函数获得的存储。请注意,知道 new
表达式必须使用哪个分配函数基本上等同于知道 void*
指向的任何内容的动态类型。如果你知道这一点,你也可以 static_cast<>
到实际类型和 delete
它...
在不首先显式调用析构函数的情况下,使用平凡的析构函数释放对象的存储是否安全?基于 [basic.life] §1.4,我会说是。请注意,如果该对象是一个数组,您可能仍然必须先调用任何数组元素的析构函数。除非它们也是微不足道的。
您能否依靠常见的编译器实现来产生您认为合理的行为?不。对您 可以 所依赖的东西有一个正式的定义,这实际上是首先拥有一个标准的全部意义所在。假设您有一个符合标准的实现,您可以依赖标准为您提供的保证。您还可以依赖特定编译器的文档可能为您提供的任何额外保证,只要您使用该特定编译器的特定版本来编译您的代码。除此之外,所有赌注都取消了……
A void
没有大小,因此编译器无法知道要释放多少内存。
编译器应如何处理以下内容?
struct s
{
int arr[100];
};
void* p1 = new int;
void* p2 = new s;
delete p1;
delete p2;
虽然标准允许实现使用传递给 delete
的类型来决定如何清理有问题的对象,但它并不要求实现这样做。该标准还将允许一种替代(并且可以说是更好的)方法,该方法基于让内存分配 new
将清理信息存储在紧接返回地址之前的 space 中,并将 delete
实现为调用类似:
typedef void(*__cleanup_function)(void*);
void __delete(void*p)
{
*(((__cleanup_function*)p)[-1])(p);
}
在大多数情况下,以这种方式实现 new
/delete
的成本相对微不足道,而且该方法会带来一些语义上的好处。这种方法的唯一重大缺点是它需要记录其 new
/delete
实现的内部工作的实现,并且其实现不能支持类型不可知的 delete
, 将不得不破坏任何依赖于他们记录的内部工作原理的代码。
请注意,如果将 void*
传递给 delete
是违反约束的,这将禁止实现提供与类型无关的 delete
,即使它们很容易做到所以,即使为他们编写的一些代码会依赖于这种能力。代码依赖于这种能力的事实使其只能移植到可以提供它的实现,当然,但是允许实现支持这种能力(如果他们选择这样做)比使其违反约束更有用。
就个人而言,我希望看到标准提供实现两个特定的选择:
允许将
void*
传递给delete
并使用传递给new
的任何类型删除该对象,并定义一个宏来指示支持此类构造.如果
void*
传递给delete
,则发出诊断,并定义一个宏指示它不支持这样的构造。
实现支持类型不可知的程序员 delete
然后可以决定他们可以从这种特性中获得的好处是否可以证明使用它所施加的可移植性限制是合理的,实现者可以决定支持更广泛的特性的好处是否合理程序的范围足以证明支持该功能的小成本。