可以在智能指针管理的内存上创建一个新的位置吗?

Is it OK to make a placement new on memory managed by a smart pointer?

上下文

出于测试目的,我需要在非零内存上构造一个对象。这可以通过以下方式完成:

{
    struct Type { /* IRL not empty */};
    std::array<unsigned char, sizeof(Type)> non_zero_memory;
    non_zero_memory.fill(0xC5);
    auto const& t = *new(non_zero_memory.data()) Type;
    // t refers to a valid Type whose initialization has completed.
    t.~Type();
}

因为这很乏味并且需要多次创建,所以我想提供一个函数,返回一个指向这样一个 Type 实例的智能指针。我想到了以下内容,但我担心某处潜伏着未定义的行为。

问题

下面的程序是否定义明确?特别是,分配了 std::byte[] 但释放了等效大小的 Type 是否是一个问题?

#include <cstddef>
#include <memory>
#include <algorithm>

auto non_zero_memory(std::size_t size)
{
    constexpr std::byte non_zero = static_cast<std::byte>(0xC5);

    auto memory = std::make_unique<std::byte[]>(size);
    std::fill(memory.get(), memory.get()+size, non_zero);
    return memory;
}

template <class T>
auto on_non_zero_memory()
{
    auto memory = non_zero_memory(sizeof(T));
    return std::shared_ptr<T>(new (memory.release()) T());
}    

int main()
{
    struct Type { unsigned value = 0; ~Type() {} }; // could be something else
    auto t = on_non_zero_memory<Type>();
    return t->value;
}

Live demo

std::shared_ptr<T>(new (memory.release()) T())

是未定义的行为。 memory 获取的内存用于 std::byte[],但 shared_ptr 的删除器正在调用 delete 指向 T 的指针。由于指针不再具有相同的类型,因此您不能根据 [expr.delete]/2

对其调用 delete

In a single-object delete expression, 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 representing a base class of such an object. If not, the behavior is undefined.

您必须为 shared_ptr 提供自定义删除器,该删除器会破坏 T,然后将指针转换回其源类型并调用 delete[]


还应注意,如果 memory 分配了一个具有非平凡破坏的类型,则 new (memory.release()) T() 本身将是未定义的。在重用它的内存之前,您必须先从 memory.release() 调用指针上的析构函数。

这个程序没有很好地定义。

规则是,如果类型有 trivial destructor (See this),则不需要调用它。所以,这个:

return std::shared_ptr<T>(new (memory.release()) T());

几乎正确。它省略了sizeof(T)std::byte的析构函数,这很好,在内存中构造了一个新的T,这很好,然后当shared_ptr准备好时删除,它调用 delete this->get();,这是错误的。这首先解构了一个 T,但随后它释放了一个 T 而不是 std::byte[],这将 可能 (未定义)不起作用。

C++ 标准 §8.5.2.4p8 [expr.new]

A new-expression may obtain storage for the object by calling an allocation function. [...] If the allocated type is an array type, the allocation function's name is operator new[].

(所有这些 "may" 是因为允许实现合并相邻的新表达式并且只为其中一个调用 operator new[],但情况并非如此 new只发生一次 (In make_unique))

与第 11 部分同节:

When a new-expression calls an allocation function and that allocation has not been extended, the new-expression passes the amount of space requested to the allocation function as the first argument of type std::size_t. That argument shall be no less than the size of the object being created; it may be greater than the size of the object being created only if the object is an array. 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.6.5) 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 ]

如果您阅读 §21.6.2 [new.delete.array],您会发现默认的 operator new[]operator delete[]operator newoperator delete,问题是我们不知道传递给它的大小,它比 delete ((T*) object) 调用的(存储大小)可能 多。

查看删除表达式的作用:

§8.5.2.5p8 [expr.delete]

[...] delete-expression will invoke the destructor (if any) for [...] the elements of the array being deleted

p7.1

If the allocation call for the new-expression for the object to be deleted was not omitted [...], the delete-expression shall call a deallocation function (6.6.4.4.2). The value returned from the allocation call of the new-expression shall be passed as the first argument to the deallocation function.

因为 std::byte 没有析构函数,我们可以安全地调用 delete[],因为它除了调用 deallocate 函数 (operator delete[]) 之外不会做任何事情。我们只需要将它重新解释回 std::byte*,我们就会得到 new[] 返回的内容。

另一个问题是如果T的构造函数抛出,就会有内存泄漏。一个简单的修复方法是在内存仍由 std::unique_ptr 拥有时放置 new,因此即使它抛出它也会正确调用 delete[]

T* ptr = new (memory.get()) T();
memory.release();
return std::shared_ptr<T>(ptr, [](T* ptr) {
    ptr->~T();
    delete[] reinterpret_cast<std::byte*>(ptr);
});

第一个放置 new 结束 sizeof(T) std::byte 的生命周期,并在同一地址开始新的 T 对象的生命周期,根据§6.6.3p5 [basic.life]

A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. [...]

然后当它被删除时,T的生命周期通过析构函数的显式调用结束,然后根据上面的delete-expression释放存储。


这导致了以下问题:

如果存储空间 class 不是 std::byte 并且不易破坏怎么办?例如,我们使用一个非平凡的联合作为存储。

调用 delete[] reinterpret_cast<T*>(ptr) 会在非对象的对象上调用析构函数。这显然是未定义的行为,并且符合 §6.6.3p6 [basic.life]

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated [...], any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. [...] The program has undefined behavior if: the object will be or was of a class type with a non-trivial destructor and the pointer is used as the operand of a delete-expression

所以要像上面那样使用它,我们必须构造它只是为了再次破坏它。

默认构造函数可能工作正常。通常的语义是"create an object that can be destructed",这正是我们想要的。使用 std::uninitialized_default_construct_n 构建它们然后立即销毁它们:

    // Assuming we called `new StorageClass[n]` to allocate
    ptr->~T();
    auto* as_storage = reinterpret_cast<StorageClass*>(ptr);
    std::uninitialized_default_construct_n(as_storage, n);
    delete[] as_storage;

我们也可以自己调用operator newoperator delete

static void byte_deleter(std::byte* ptr) {
    return ::operator delete(reinterpret_cast<void*>(ptr));
}

auto non_zero_memory(std::size_t size)
{
    constexpr std::byte non_zero = static_cast<std::byte>(0xC5);

    auto memory = std::unique_ptr<std::byte, void(*)(std::byte*)>(
        reinterpret_cast<std::byte*>(::operator new(size)),
        &::byte_deleter
    );
    std::fill(memory.get(), memory.get()+size, non_zero);
    return memory;
}

template <class T>
auto on_non_zero_memory()
{
    auto memory = non_zero_memory(sizeof(T));
    T* ptr = new (memory.get()) T();
    memory.release();
    return std::shared_ptr<T>(ptr, [](T* ptr) {
        ptr->~T();
        ::operator delete(ptr, sizeof(T));
                            // ^~~~~~~~~ optional
    });
}

但这看起来很像 std::mallocstd::free

第三种解决方案可能是使用 std::aligned_storage 作为给定 new 的类型,并让删除器像 std::byte 一样工作,因为对齐存储是一个微不足道的聚合。