在不调用析构函数的情况下结束 STL 容器的生命周期

Posted

技术标签:

【中文标题】在不调用析构函数的情况下结束 STL 容器的生命周期【英文标题】:Ending lifetime of STL container without calling the destructor 【发布时间】:2014-12-06 14:18:13 【问题描述】:

问题

C++11 标准是否允许结束容器的生命周期 (例如,std::map)不调用它的析构函数,如果这样的话 容器不需要调用它的元素的析构函数 包含并且不需要释放内存(使用 分配器::deallocate)。

深入解释

C++11 标准规定如下:

程序可以通过重用对象占用的存储空间或通过显式调用具有非平凡析构函数的类类型对象的析构函数来结束任何对象的生命周期。对于具有非平凡析构函数的类类型的对象,在重用或释放对象占用的存储空间之前,程序不需要显式调用析构函数;但是,如果没有显式调用析构函数,或者如果没有使用删除表达式 (5.3.5) 来释放存储,则不应隐式调用析构函数以及依赖于析构函数产生的副作用的任何程序有未定义的行为。

这是清晰明了的。 例如,有一些对象在其生命周期内分配内存并在销毁时释放内存。如果程序依赖于释放内存,那么不调用对象的析构函数会导致未定义的行为。另一方面,如果对象从某个内存池中获取内存,则不需要调用析构函数,因为程序不依赖于它的副作用并且行为是明确定义的。

但是像 std::map、std::list 等 STL 容器呢? 标准规定,符合要求的实现必须遵循 AS-IF 规则。只要可观察的行为相同,实现可能会有所不同。

我想说的是,例如,如表 96(容器要求)中所述,容器的析构函数应调用其元素的析构函数并释放所有内存。但是如果它内部也使用了一些互斥锁怎么办。标准不禁止在容器内使用一个(我错了吗?)。不调用 mutex 的析构函数可能会导致未定义的行为。

我想知道,标准是否允许使用 std::map 并在不调用析构函数的情况下结束其生命周期。例如,std::map 使用自定义分配器。这个分配器使用一些内存池,释放内存不需要释放函数。由于容器中的所有内存都是使用这个分配器获得的,因此使用这种容器的程序不会依赖析构函数的副作用。

代码:

class MemoryPool

public:
    ...

    // Pre-allocates memory.
    // Returns true on success.
    bool initialize(uint32_t size)
    
        ...
    

    // Returns properly aligned block of memory from pre-allocated area.
    template <class T> T* allocate(size_t n = 1)
    
        ...
    

    ...
;

template <class T> class CustomAllocator

public:
    CustomAllocator(MemoryPool& memoryPool): memoryPool_(&memoryPool) 

    ...

    /* this allocator obtains memory from memory pool */
    T* allocate(size_t n)
    
        return memoryPool_->allocate<T>(n);
    

    // This function may be a no-op, it depends on the implementation of
    // memory pool. It doesn't really matter in context of this question.
    // Anyway, all memory is already allocated in memory pool, so whether there
    // is a need to mark unused chunks or not depends on actual application.
    void deallocate(T*, size_t) 
    ...

private:
    MemoryPool* memoryPool_;
    ...
;

MemoryPool memoryPool;
memoryPool.initialize();

// I intentionally use only fundamental types in this map
// since not invoking the destructors of such objects
// will not lead to undefined behavior
typedef std::map
<
    uint32_t, uint32_t,
    std::less<uint32_t>,
    CustomAllocator<uint32_t>
> SomeMap;

SomeMap* someMap = memoryPool.allocate<SomeMap>();
new(someMap) SomeMap(CustomAllocator<uint32_t>memoryPool);

// no destructor of SomeMap is called
// memory is deallocated in destructor of memory pool

【问题讨论】:

你在哪里隐藏了实际问题? 我已经高亮了句子 我相信这种情况的通常做法是让分配器的deallocate() 成为空操作。 致 Mat:这与 malloc 和 free 无关。这是关于 std::map 析构函数的副作用。在上面的示例中,所有内存都已在池中预分配(使用 operator new)。基本上,在 MemoryPool::initialize 中有类似 storage_ = new uint8_t[storageSize] 的东西。从 MemoryPool 的析构函数中调用运算符 delete。这就是为什么不需要从容器内释放内存的原因。 @Mat 问题是关于它是否明确定义为 not 调用map 的析构函数。如果您确实调用了析构函数,那最终将执行大量工作(清理自己的内部),这在 OP 的情况下最终是无用的,因此询问是否允许省略对该析构函数的调用似乎是一个公平的问题我。 【参考方案1】:

我在这里问过这个问题:https://groups.google.com/a/isocpp.org/forum/#!topic/std-discussion/R_KPazXbE0k

根据 17.5.2.3 / 1:

第 18 到 30 条和附录 D 未指定类的表示,并有意省略 类成员的规范(9.2)。实现可以定义静态或非静态类成员,或者 两者,根据需要实现第 18 到 30 条中指定的成员函数的语义,以及 附录 D。

换句话说,实现可能有一些私有成员使用内存以外的资源。因此,不能保证没有其他副作用(至少在当前的 C++11 标准中)。

【讨论】:

以上是关于在不调用析构函数的情况下结束 STL 容器的生命周期的主要内容,如果未能解决你的问题,请参考以下文章

在一个派生类对象结束其生命周期时析构函数的调用顺序

析构函数小结

C++ 析构函数何时被调用?

C++ 析构函数何时被调用?

2022-04-09 STL容器vector与拷贝构造函数

如何在不破坏移动和复制构造函数的情况下声明虚拟析构函数