代码之家  ›  专栏  ›  技术社区  ›  YSC

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

  •  21
  • YSC  · 技术社区  · 6 年前

    {
        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[] 只分配了一个 同等大小的释放是否存在问题?

    #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 回复  |  直到 6 年前
        1
  •  25
  •   Artyer    6 年前

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

    trivial destructor (见 this

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

    对的它省略了 sizeof(T) std::byte T 在记忆中,这是美好的,然后当 shared_ptr 准备删除时,它会调用 delete this->get(); ,这是错误的。这首先解构了 ,但随后它会释放一个 而不是 std::byte[] ,这将 可能

    C++标准符号:85.2.4p8[ Exp.New ]

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

    新操作员[] new 只发生一次 make_unique

    以及同一节的第11部分:

    当新表达式调用分配函数且该分配尚未扩展时,新表达式将请求的空间量作为类型的第一个参数传递给分配函数 std::size_t . 该参数不得小于所创建对象的大小;仅当对象是数组时,它可能大于正在创建的对象的大小。对于数组 char unsigned char ,新表达式的结果与 函数被假定为返回指向存储的指针,这些指针对于具有基本对齐方式的任何类型的对象都是适当对齐的,这种对数组分配开销的约束允许使用分配的常见习惯用法 其他类型的对象稍后将放置到的字符数组中。[完注]

    如果您阅读§21.6.2[新建.删除.数组],您会看到默认 新操作员[] operator delete[] operator new operator delete ,问题是我们不知道传递给它的大小,它是 可能 delete ((T*) object) 调用(以存储大小)。

    查看删除表达式的作用:

    §8.5.2.5p8[解释删除]

    […]delete表达式将为要删除的数组的[…]元素调用析构函数(如果有)

    p7.1

    如果未忽略要删除对象的新表达式的分配调用[…],则删除表达式应调用解除分配函数(6.6.4.4.2)。新表达式的分配调用返回的值应作为第一个参数传递给释放函数。

    自从 std::字节 delete[] ,因为它除了调用deallocate函数之外不会执行任何操作( 运算符删除[] ).我们只需要重新解释一下 std::byte* ,我们会得到什么 new[]

    另一个问题是,如果 T 投掷。一个简单的修复方法是放置 而内存仍由 std::unique_ptr 删除[] 正确地

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

    刚出现的 生命周期结束 sizeof(T) std::字节 开始一个新的生命

    然后,当它被删除时 通过显式调用析构函数结束,然后根据上面的说明,delete表达式取消分配存储。


    这导致了以下问题:

    如果存储类不是 std::字节 ,并不是微不足道的破坏?例如,我们使用一个非平凡的联合作为存储。

    使命感 delete[] reinterpret_cast<T*>(ptr) 将对非对象的对象调用析构函数。这显然是未定义的行为,符合§6.6.3p6[基本寿命]

    可以使用,但只能以有限的方式使用。[…]如果:对象将是或曾经是具有非平凡析构函数的类类型,并且指针用作删除表达式的操作数,则程序具有未定义的行为

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

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

    新接线员 运算符删除

    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::free .

    std::aligned_storage 与给定的类型相同 刚出现的 std::字节 因为对齐存储是一个微不足道的聚合。

        2
  •  15
  •   NathanOliver    6 年前
    std::shared_ptr<T>(new (memory.release()) T())
    

    是未定义的行为。记忆由计算机获得的记忆 memory 是为了一个 std::byte[] shared_ptr 的删除程序正在进行调用 delete T . 由于指针不再具有相同的类型,因此不能根据调用对其进行删除 [expr.delete]/2

    在单对象delete表达式中,delete操作数的值可以是空指针值、指向由以前的新表达式创建的非数组对象的指针,或者指向表示此类对象基类的子对象的指针。如果不是,则行为未定义。

    你必须提供 共享ptr 使用自定义的删除程序 T 然后将指针转换回其源类型并调用 delete[]


    还应指出的是 new (memory.release()) T() 如果 已分配具有非平凡破坏的类型。您必须从中调用指针上的析构函数 memory.release() 首先,在重用它的内存之前。