问题描述
我正在尝试编写一个符合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
来激活该成员。
然后,您可以使用展示位置-new
在T
中创建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;
};