首页 > 解决方案 > 可以在智能指针管理的内存上创建新的位置吗?

问题描述

语境

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

{
    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实例的智能指针。我想出了以下内容,但我担心未定义的行为潜伏在某处。

问题

以下程序是否定义明确?特别是,astd::byte[]已被分配但Type大小相等的 a 被释放是一个问题吗?

#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;
}

现场演示

标签: c++language-lawyerc++17smart-pointersundefined-behavior

解决方案


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

规则是,如果一个类型有一个平凡的析构函数(见这个),你不需要调用它。所以这:

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

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

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

new-expression 可以通过调用分配函数来获取对象的存储空间。[...] 如果分配的类型是数组类型,则分配函数的名称是operator new[].

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

以及同一部分的第 11 部分:

当 new 表达式调用分配函数并且该分配尚未扩展时,new 表达式将请求的空间量作为 type 的第一个参数传递给分配函数std::size_t。该参数不应小于正在创建的对象的大小;只有当对象是一个数组时,它才可能大于正在创建的对象的大小。对于charunsigned char和的数组std::byte,new-expression 的结果与分配函数返回的地址之间的差应是大小不大于数组大小的任何对象类型的最严格基本对齐要求 (6.6.5) 的整数倍正在创建。[注意:因为分配函数被假定返回指向存储的指针,该指针针对具有基本对齐的任何类型的对象进行了适当对齐,因此对数组分配开销的这种约束允许分配字符数组的常见习惯用法,稍后将在其中放置其他类型的对象. ——尾注]

如果您阅读 §21.6.2 [new.delete.array],您会看到默认值operator new[]并执行与andoperator delete[]完全相同的操作,问题是我们不知道传递给它的大小,它可能不止于什么调用(存储大小)。operator newoperator deletedelete ((T*) object)

查看删除表达式的作用:

§8.5.2.5p8 [expr.delete]

[...] delete-expression 将为 [...] 被删除的数组元素调用析构函数(如果有)

p7.1

如果对要删除的对象的新表达式的分配调用没有省略[...],则删除表达式应调用释放函数(6.6.4.4.2)。从 new-expression 的分配调用返回的值应作为第一个参数传递给释放函数。

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

另一个问题是,如果Tthrows 的构造函数存在内存泄漏。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);
});

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

程序可以通过重用对象占用的存储空间或通过显式调用具有非平凡析构函数的类类型对象的析构函数来结束任何对象的生命周期。[...]

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


这导致了以下问题:

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

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

在对象的生命周期开始之前,但在分配对象将占用的存储空间之后[...],可以使用任何表示对象将要或曾经位于的存储位置的地址的指针,但只能在有限的方式。[...] 程序在以下情况下具有未定义的行为:对象将是或曾经是具有非平凡析构函数的类类型,并且指针用作删除表达式的操作数

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

默认构造函数可能工作正常。通常的语义是“创建一个可以被破坏的对象”,这正是我们想要的。用于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 newandoperator 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::mallocand std::free

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


推荐阅读