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

标签 c++ language-lawyer c++17 smart-pointers undefined-behavior

上下文

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

{
    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

最佳答案

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

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

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

几乎是正确的。它省略了 sizeof(T) 的析构函数std::byte s,这很好,构造一个新的 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[].

(所有这些“可能”是因为允许实现合并相邻的新表达式,并且只为其中一个调用 operator new[],但情况并非如此,因为 new 只发生一次(在 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 new 完全相同的事情和 operator 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的构造函数存在内存泄漏抛出。一个简单的解决方法是放置 new而内存仍归 std::unique_ptr 所有, 所以即使它抛出它也会调用 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 s 并开始一个新的 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 释放存储空间。


这就引出了以下问题:

如果存储类不是 std::byte 怎么办? ,并且不是可以轻易破坏的吗?例如,我们使用非平凡的 union 作为存储。

调用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

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

默认构造函数可能工作正常。通常的语义是“创建一个可以被破坏的对象”,这正是我们想要的。使用 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 一样工作因为对齐的存储是一个微不足道的聚合。

关于c++ - 可以在智能指针管理的内存上创建新的位置吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54095233/

相关文章:

javascript - 如何使用 JavaScriptCore 将 C++ 对象传递给 JavaScript 函数

c++ - 从另一个 makefile 追加/替换 Makefile 中的字符串

c++ - WinAPI - 如果 .ini 文件不存在则创建它

c++ - 迭代器和简单的赋值/析构函数

c - #if 指令 : macro expansion vs the "defined" keyword 中的评估顺序

c++ - 我真的需要为const对象实现用户提供的构造函数吗?

一对指向采用不同类型参数的不同函数的指针可以兼容吗?

c++ - std::visit 不接受作为派生类对象的可调用对象

c++ - 为什么结构化绑定(bind)只适用于自动

c++ - 可以在 C++11 中模拟 std::is_invocable 吗?