正确重用联合成员的存储并强制转换所涉及的指针对象池

问题描述

我正在尝试编写一个符合C ++ 20标准的对象池,该对象池依赖于对象模型周围的新措辞,从而消除了一些未定义的行为。注释显示了我用于推理(https://timsong-cpp.github.io/cppwp/n4861)的标准草案中的段落。

创建时,池为固定数量的对象分配存储,并管理未使用存储中的空闲列表。现在,我假设类型T没有const或引用非静态成员。

#include <iostream>
#include <stdexcept>
#include <type_traits>

template <typename T>
class ObjectPool {
public:
    using value_type = T;

    ObjectPool(std::ptrdiff_t capacity) :
        m_capacity(capacity),m_nodes(
            // Cast the result pointer back to Node* (https://timsong-cpp.github.io/cppwp/n4861/expr.static.cast#13)
            static_cast<Node*>(
                /*
                Implicitly creates (https://timsong-cpp.github.io/cppwp/n4861/basic.memobj#intro.object-10 and https://timsong-cpp.github.io/cppwp/n4861/basic.memobj#intro.object-13):
                    * the Node[capacity] array
                    * the Node union objects
                    * the Node* member subobjects

                Returns a pointer to the array casted to void* (https://timsong-cpp.github.io/cppwp/n4861/basic.memobj#intro.object-11)
                */
                operator new(capacity * sizeof(Node))
            )
        )
    {
        /*
        The implicit creations happen because it makes the following code defined behaviour.
        Otherwise it would be UB because:
            * Pointer arithmetic without them pointing to elements of an Node[capacity] array (https://timsong-cpp.github.io/cppwp/n4861/expr.add#4)
            * Accessing Node objects through 'pointer to object' pointers outside their lifetime (https://timsong-cpp.github.io/cppwp/n4861/basic.life#6.2).
            * Accessing the Node* subobjects through 'pointer to object' pointers outside their lifetime.
        */

        // Add all nodes to the freelist.
        Node* next = nullptr;
        for (std::ptrdiff_t i = m_capacity - 1; i >= 0; --i) {
            m_nodes[i].next = next;
            next = &m_nodes[i];
        }
        m_root = next;
    }

    ~ObjectPool()
    {
        /*
        Release the allocated storage.
        This ends the lifetime of all objects (array,Node,Node*,T) (https://timsong-cpp.github.io/cppwp/n4861/basic.life#1.5).
        */
        operator delete(m_nodes);
    }

    template <typename... Args>
    T* create(Args&&... args)
    {
        // freelist is empty
        if (!m_root) throw std::bad_alloc();

        Node* new_root = m_root->next;

        /*
        Activate the 'storage' member (https://timsong-cpp.github.io/cppwp/n4861/class.union#7).
        Is this strictly necessary?
        */
        new(&m_root->storage) Storage;

        /*
        Create a T object in the storage of the std::aligned_storage object (https://timsong-cpp.github.io/cppwp/n4861/basic.memobj#intro.object-1).

        This ends the lifetime of the std::aligned_storage object (https://timsong-cpp.github.io/cppwp/n4861/basic.life#1.5)?
        Because std::aligned_storage is most likley implemented with a unsigned char[N] array (https://timsong-cpp.github.io/cppwp/n4861/meta.trans.other#1),it 'provides storage' (https://timsong-cpp.github.io/cppwp/n4861/intro.object#3)
        for the T object and so the T object is 'nested within' (https://timsong-cpp.github.io/cppwp/n4861/intro.object#4.2) the std::aligned_storage
        which does not end its lifetime.
        This means without knowing the implementation of std::aligned_storage I don't know if the lifetime has ended or not?

        The union object is still in it's lifetime? The T object is 'nested within' the union object because it is
        'nested within' the member subobject 'storage' because that 'provides storage' (https://timsong-cpp.github.io/cppwp/n4861/intro.object#4.3).

        The union has no active members (https://timsong-cpp.github.io/cppwp/n4861/class.union#2).
        */
        T* obj = new(&m_root->storage) T{std::forward<Args>(args)...};

        m_root = new_root;
        return obj;
    }

    void destroy(T* obj)
    {
        /* Destroy the T object,ending it's lifetime (https://timsong-cpp.github.io/cppwp/n4861/basic.life#5). */
        obj->~T();

        /* if std::aligned_storage is in its lifetime.

        T represents the first byte of storage and is usable in limited ways (https://timsong-cpp.github.io/cppwp/n4861/basic.life#6).
        The storage pointer points to the std::aligned_storage object (https://timsong-cpp.github.io/cppwp/n4861/expr.reinterpret.cast#7 and https://timsong-cpp.github.io/cppwp/n4861/expr.static.cast#13).

        I'm not sure is std::launder() is necessary here because we did not create a new object.

        Storage* storage = reinterpret_cast<Node*>(storage);
        */

        /* if std::aligned_storage is not in its lifetime.

        Create a std::aligned_storage object in the storage of the former T object (https://timsong-cpp.github.io/cppwp/n4861/basic.memobj#intro.object-1).

        This activates the 'storage' member of the corresponding union (https://timsong-cpp.github.io/cppwp/n4861/class.union#2).
        */
        Storage* storage = new(obj) Storage;

        /*
        Get a pointer to the union from a pointer to a member (https://timsong-cpp.github.io/cppwp/n4861/basic.compound#4.2).
        */
        Node* node = reinterpret_cast<Node*>(storage);

        /*
        Activate the 'next' member creating the corresponding subobject (https://timsong-cpp.github.io/cppwp/n4861/class.union#6),the lifetime of the 'storage' subobject ends.
        */
        node->next = m_root;
        m_root = node;
    }

    std::ptrdiff_t capacity() const
    {
        return m_capacity;
    }
private:
    using Storage = typename std::aligned_storage<sizeof(T),alignof(T)>::type;

    union Node {
        Node* next;
        Storage storage;
    };
    std::ptrdiff_t m_capacity;
    Node* m_nodes;
    Node* m_root;
};

struct Block {
    long a;
    std::string b;
};

int main(int,char **)
{
    ObjectPool<Block> pool(10);

    Block* ptrs[10];
    for (int i = 0; i < 10; ++i) {
        ptrs[i] = pool.create(i,std::to_string(i*17));
    }
    std::cout << "Destroying objects\n";
    for (int i = 0; i < 10; ++i) {
        std::cout << ptrs[i]->a << " " << ptrs[i]->b << "\n";
        pool.destroy(ptrs[i]);
    }
    return 0;
}

我最大的问题是了解如何将T*函数中的destroy(T*)指针转换为指向可用Node*对象的Node指针,可以将其添加到空闲列表中?

如果对象和子对象使用完全相同的存储区域(联合及其成员),并且我重复使用成员的存储,我也将无法理解它们的工作方式。子对象(成员)的生存期结束,但是父对象(联合)是否保留了其生存期,即使其所有存储都被重用了?

解决方法

处理此问题的方式不必要地过度设计。它仍然可以工作,但是在大多数情况下,您所谈论的关于隐式对象创建(IOC)的特定更改与您的代码无关。或者更确切地说,您可以在不依赖IOC的情况下做您想做的事情(从而编写可以在C ++ 17下运行的代码)。

所以让我们从头开始:您的内存分配。

您分配了一堆内存。但是您的目标是分配一个Node数组。所以... 只要这样做。只需调用new Node[capacity]即可,而不是分配未格式化的内存。依靠IOC来解决您可以轻松解决自己的问题是没有意义的(可以说结果对所发生的事情更具可读性)。

因此,在分配数组之后,将一堆值放入其中。您可以通过使用next联合体的Node成员来实现。之所以有效,是因为union的第一个成员在创建时始终处于活动状态(除非您执行特殊操作)。因此,所有Node对象的next成员都处于活动状态。

现在,让我们继续创建T。您要激活Node::storage。在这种情况下,放置new可以使用,但是即使使用IOC,您仍然需要它。也就是说,IOC不会更改union的规则。工会会员只能implicitly be activated by an assignment to the named member。而且您不是要这样做;您将只使用其地址。因此,您仍然需要调用placement-new来激活该成员。

然后,您可以使用展示位置-newT中创建storage本身。现在我们来谈一生。

您引用[basic.life]/1.5来建议,一旦这样做,storage的生命周期就会结束。的确如此,但这仅是因为您使用了aligned_storage_t

让我们假设您使用std::aligned_storage_t代替alignas(T) unsigned char[sizeof(T)]作为storage的类型。这很重要,因为字节数组具有特殊的行为。

如果storage被这样定义,那么T嵌套在 storage中。在[intro.object]/4.2中,我们看到:

对象 a 嵌套在另一个对象 b ,如果:

...

  • b a
  • 提供存储

...

从上一段中,我们学习:

如果在与另一个对象关联的存储中创建了一个完整对象([expr.new]),则该对象e的类型为“ N个无符号字符数组”或“ N个std :: byte数组”类型([cstddef。 syn]),则在以下情况下,该数组将为创建的对象提供存储空间:

  • e的寿命已经开始并且没有结束,并且
  • 新对象的存储空间完全适合e,并且
  • 没有满足这些约束的较小数组对象。

如果您使用字节数组,所有这些都是正确的,因此即使在其中创建了storage之后,T仍将继续存在。

如果这听起来像是不使用std::aligned_storage的充分理由,那是因为。

由于所有这些都是有效的C ++ 17,因此,如果您切换到对齐的字节数组,则不必担心。 storage将继续存在。

现在我们要删除。摧毁T是您要做的第一件事。

因此,您有一个指向(刚刚销毁的)对象的指针,但是您需要获得一个指向Node的指针。这是一个问题,因为...嗯,您没有一个。我的意思是,是的,T的地址与storage的地址相同,该地址可以与指向Node的指针进行指针互换。但是,这就是从指针到T到指针到storage的第一步。 reinterpret_cast无法带您到达那里。

但是std::launder可以。而且您可以直接从T*Node*,因为两个对象都具有相同的地址,并且Node在其生存期内。

拥有Node*后,您可以重新激活该对象的next成员。而且由于您可以通过分配来完成,所以无需放置-new。因此,该功能中的大多数内容都是不必要的。

同样,这对于C ++ 17完全有效。甚至是implicit activation of a union member is standard C++17(规则稍有不同,但此处不适用差异)。


因此,让我们看一下代码的有效C ++ 17版本:


#include <cstddef>
#include <new>


template <typename T>
class ObjectPool {
public:
    using value_type = T;

    ObjectPool(std::ptrdiff_t capacity)
        : capacity_(capacity),nodes_(new Node[capacity])
    {
        // Add all nodes to the freelist.
        Node* next = nullptr;
        for (std::ptrdiff_t i = capacity_ - 1; i >= 0; --i) {
            nodes_[i].next = next;
            next = &nodes_[i];
        }
        root_ = next;
    }

    ~ObjectPool()
    {
        delete[] nodes_;
    }

    template <typename... Args>
    T* create(Args&&... args)
    {
        // freelist is empty
        if (!root_) throw std::bad_alloc();

        auto *allocate = root_;
        root_ = root_->next;

        new(&allocate->storage) decltype(allocate->storage);

        //Note: not exception-safe.
        T* obj = new(&allocate->storage) T(std::forward<Args>(args)...);
        return obj;
    }

    void destroy(T* obj)
    {
        obj->~T();
        
        Node *free = std::launder(reinterpret_cast<Node*>(obj));
        
        free->next = root_;
        root_ = free;
    }

    std::ptrdiff_t capacity() const
    {
        return capacity_;
    }
    
private:
    union Node
    {
        Node* next;
        alignas(T) std::byte storage[sizeof(T)];
    };
    
    std::ptrdiff_t capacity_;
    Node* nodes_;
    Node* root_ = nullptr;
};

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...