可以在由智能指针管理的内存中重新放置新的位置吗?

时间:2019-01-08 15:47:10

标签: 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

2 个答案:

答案 0 :(得分:21)

该程序定义不明确。

规则是,如果类型具有trivial destructor(请参见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]

  

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

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

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

  

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

如果您阅读§21.6.2[new.delete.array],则会看到默认的operator new[]operator delete[]做的事与operator new和{{1} },问题是我们不知道传递给它的大小,并且它可能比operator delete调用的(存储大小)更多

查看删除表达式的作用:

§8.5.2.5p8[expr.delete]

  

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

p7.1

  

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

由于delete ((T*) object)没有析构函数,因此我们可以安全地调用std::byte,因为它除了调用deallocate函数(delete[])之外不会做其他任何事情。我们只需要将其重新解释为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)的生存期,并根据相同的第6.6节开始在相同地址的新std::byte对象的生存期.3p5 [basic.life]

  

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

然后,在删除它时,T的生存期将由析构函数的显式调用结束,然后根据上述内容,delete-expression将释放存储。


这导致了以下问题:

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

调用std::byte将在非对象的对象上调用析构函数。这显然是不确定的行为,符合§6.6.3p6[basic.life]

  

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

因此要像上面一样使用它,我们必须构造它以再次破坏它。

默认构造函数可能工作正常。通常的语义是“创建可以破坏的对象”,这正是我们想要的。使用std::uninitialized_default_construct_n来构建它们,然后立即销毁它们:

delete[] reinterpret_cast<T*>(ptr)

我们还可以自己拨打 // 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 new

operator 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::malloc

第三个解决方案可能是使用std::aligned_storage作为给std::free的类型,并让删除器与new一起工作,因为对齐的存储是无关紧要的聚合。

答案 1 :(得分:15)

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

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

在其上调用delete
  

在单对象删除表达式中,删除操作数的值可以为空指针值,指向由先前的new表达式创建的非数组对象的指针或指向表示基数的子对象的指针这样的对象的类。如果不是,则行为是不确定的。

您必须为shared_ptr提供一个自定义删除程序,该删除程序将销毁T,然后将指针投射回其源类型,并在其上调用delete[]


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