[英]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- new
在storage
中创建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中,如果:
...
- b为a提供存储,或
...
从上一段中,我们了解到:
如果一个完整的 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.