在不调用替换运算符 delete 的情况下取消分配由全局替换运算符 new 返回的指针是未定义的行为吗? (C++17)

Is it undefined behavior to deallocate a pointer returned by a global replacement operator new, without calling a replacement operator delete? (C++17)

在 SL operator new 未返回的非空指针上调用 SL operator delete 被视为未定义行为,如此处针对 (1) 和 (2) 所述: https://en.cppreference.com/w/cpp/memory/new/operator_delete

The behavior of the standard library implementation of this function is undefined unless ptr is a null pointer or is a pointer previously obtained from the standard library implementation of operator new(size_t) or operator new(size_t, std::nothrow_t).

因此,混合使用 operator newoperator deleteoperator new[]operator delete[] 也是未定义的行为。我在标准中找不到任何说明这是否也适用于调用用户分配方法的替换 operator newoperator delete。例如:

void* operator new(std::size_t p_size)
{
    void *ptr = UserImplementedAlloc(p_size);
    return ptr;
}

void* operator new[](std::size_t p_size)
{
    void *ptr = UserImplementedAlloc(p_size);
    return ptr;
}

void operator delete(void* p_ptr)
{
    UserImplementedFree(p_ptr);
}

void operator delete[](void* p_ptr)
{
    UserImplementedFree(p_ptr);
}

以下是否未定义?假设 UserImplementedAlloc 总是 returns 一个正确的地址,而不是 nullptr.

struct Simple
{
    explicit Simple(); //Allocates m_bytes
    ~Simple(); //Frees m_bytes
    char * m_bytes;    
};

/*Placement new is not replaced or overridden for these examples.*/

//Example A
{
    //Allocates and invokes constructor
    Simple* a = new Simple();
    //Invokes destructor
    a->~Simple();
    //Deallocates
    UserImplementedFree(static_cast<void*>(a));
}

//Example B
{
    //Allocates
    void* addr = UserImplementedAlloc(sizeof(Simple));
    //Invokes constructor
    Simple* b = new (addr) Simple();
    //Invokes destructor and deallocates
    delete b;
}

我不是在寻找关于这是否是不良做法的讲座,我只是想确定这是否是定义的行为。

您的编译器版本的 delete 可能知道一些隐藏在实现中的关于依赖于类型的已删除指针的信息。

首先手动调用析构函数然后删除一个 void*(因为否则你会调用析构函数两次)是不安全的。您没有删除 C++ 语义中的相同指针。它在汇编级别是相同的地址,它可能会释放相同数量的内存——或者,你真的知道吗?你愚弄了编译器删除了一个 void* 而不是实际类型。

两个例子都是未定义的行为。现在我已经花时间浏览了 C++17 standard final draft,我找到了我需要的证据。


示例 A

关于operator new

分配函数 - § 6.7.4.1.2

If the request succeeds, the value returned shall be a non-null pointer value (7.11) p0 different from any previously returned value p1, unless that value p1 was subsequently passed to an operator delete

在示例 A 中,我们调用 new-expressionSimple* a = new Simple(),它在内部将调用适当的 operator new。我们在调用 UserImplementedFree(static_cast<void*>(a)) 时绕过 operator delete。尽管 operator delete 会调用此函数,并且可能会执行相同的释放操作,但要注意的是,对 operator new 的任何后续调用现在都可能 return 一个与 [=19] 匹配的地址的指针=] 有。 a 从未传递给 operator delete。所以我们违反了上述规则。


示例 B

删除表达式 - § 8.3.5.2

...the value of the operand of delete may be a null pointer value, a pointer to a non-array object created by a previous new-expression, or a pointer to a subobject (4.5) representing a base class of such an object (Clause 13). If not, the behavior is undefined. In the second alternative (delete array), the value of the operand of delete may be a null pointer value or a pointer value that resulted from a previous array new-expression. 83 If not, the behavior is undefined.

在示例 B 中,我们不通过 new-expression 分配 addr。然后我们尝试使用 delete-expression 来释放它。这违反了上述规则。


定义的行为是什么样的?

这些例子的主要特点是构造和分配的分离,以及销毁和释放的分离。该标准规定如下:

新表达式 - § 8.3.4.11

For arrays of char, unsigned char, and std::byte, the difference between the result of the new-expression and the address returned by the allocation function shall be an integral multiple of the strictest fundamental alignment requirement (6.11) of any object type whose size is no greater than the size of the array being created. [ Note: Because allocation functions are assumed to return pointers to storage that is appropriately aligned for objects of any type with fundamental alignment, this constraint on array allocation overhead permits the common idiom of allocating character arrays into which objects of other types will later be placed. — end note ]

所以定义的行为可能看起来像这样:

{
    //Allocates bytes
    char* bytes = new char[sizeof(Simple)];
    //Invokes constructor
    Simple* a = new ((void *)bytes) Simple();
    //Invokes destructor
    a->~Simple();
    //Deallocates
    delete[] bytes;
}

再说一次,不一定是好的做法,而是明确的行为。