繁体   English   中英

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

[英]Correctly reusing storage of a union member and casting the involved pointers (object pool)

我正在尝试编写一个符合 object 池的 C++20 标准,该池依赖于 object model 周围的新措辞,从而消除了一些未定义的行为。 注释显示了我用于推理的标准草案段落( 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;
}

我最大的问题是要了解我必须做什么才能将destroy(T*) function 中的T*指针转换为指向可用Node object 的Node*指针,这样可以将其添加到freelist 中吗?

如果对象和子对象使用完全相同的存储区域(联合及其成员)并且我重用成员的存储,我也不明白它们是如何工作的。 子对象(成员)的生命周期结束,但父对象 object(联合)是否仍然在其生命周期内,尽管其所有存储都被重用了?

你这样做的方式是不必要的过度设计。 它仍然可以工作,但是您谈论的关于隐式 object 创建 (IOC) 的具体更改在很大程度上与您的代码无关。 或者更确切地说,您可以在不依赖 IOC 的情况下做您正在尝试的事情(从而编写在 C++17 下运行的代码)。

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

你分配了一堆memory。 但是您的目标是分配一个Node数组。 所以……就那样做吧 只需调用new Node[capacity] ,而不是分配未形成的 memory。 依靠IOC来解决您自己可以轻松解决的问题是没有意义的(可以说结果对于正在发生的事情更具可读性)。

因此,在分配数组之后,您将一堆值放入其中。 您可以通过使用Node联合的next成员来执行此操作。 这是有效的,因为union的第一个成员在创建时总是活跃的(除非你做了一些特别的事情)。 所以所有的Node对象都有next活跃的成员。

现在,让我们继续创建T 您想激活Node::storage 在这种情况下放置new作品,但即使使用 IOC,您仍然需要它。 也就是说,IOC 不会改变union的规则。 联合成员只能通过对命名成员的赋值来隐式激活 而且您并没有尝试这样做; 你只会使用它的地址。 所以你仍然需要placement- new调用来激活成员。

然后,您可以使用 placement- newstorage中创建T本身。 现在我们开始谈论生命。

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

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

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

一个 object a嵌套在另一个 object b中,如果:

...

  • ba提供存储,或

...

从上一段中,我们了解到:

如果一个完整的 object 在与另一个 object e 类型“N 无符号字符数组”或“N 数组 std::byte”([cstddef.syn]) 关联的存储中创建 ([expr.new]),则阵列为创建的 object 提供存储,如果:

  • e 的生命周期已经开始但没有结束,并且
  • 新 object 的存储完全适合 e,并且
  • 没有更小的数组 object 可以满足这些约束。

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

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

并且由于所有这些都是有效的C++17,如果你切换到对齐的字节数组,你不必担心; storage将继续在其生命周期内。

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

所以你有一个指向(刚刚销毁的)object 的指针,但是你需要一个指向Node的指针。 这是一个问题,因为...好吧,你没有。 我的意思是,是的, T的地址与storage的地址相同,后者可以与指向Node的指针相互转换。 但这是该过程的第一步,从指向T的指针到指向storage的指针,这就是问题所在。 reinterpret_cast无法让您到达那里。

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

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

再一次,这对 C++17 完全有效。 甚至工会成员的隐式激活也是标准的 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;
};

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM