通过 reinterpret_cast 和 std::launder 进行存储访问
Storage access via reinterpret_cast and std::launder
我创建了一个内存池,以便将其用作对象的新位置,并且我使用空闲槽来获得空闲列表以便重用这些槽:
template<class T>
class ObjectPool {
public:
ObjectPool( std::size_t cap );
virtual ~ObjectPool();
void* allocate() noexcept(false);
void deallocate( void* ptr );
private:
template<typename M>
static constexpr const M& max( const M& a, const M& b ) {
return a < b ? b : a;
}
using storage = typename std::aligned_storage<max(sizeof(T), sizeof(T*)), std::alignment_of<T>::value>::type;
storage* pool;
std::mutex mutex;
std::size_t capacity;
std::size_t counter;
T* deletedItem;
};
template<class T>
ObjectPool<T>::ObjectPool( std::size_t cap ) :
capacity(cap), counter(0), deletedItem(nullptr) {
if ( capacity > 0 )
pool = ::new storage[cap];
else
pool = nullptr;
}
template<class T>
ObjectPool<T>::~ObjectPool() {
::delete[] pool;
}
template<class T>
void* ObjectPool<T>::allocate() noexcept(false) {
std::lock_guard<std::mutex> l(mutex);
if ( deletedItem ) {
T* result = deletedItem;
deletedItem = *(reinterpret_cast<T**>(deletedItem)); //<-----undefined behavior??
return result;
}
if ( counter >= capacity ) {
throw std::bad_alloc();
}
return std::addressof(pool[counter++]);
}
template<class T>
void ObjectPool<T>::deallocate( void* ptr ) {
std::lock_guard<std::mutex> l(mutex);
*(reinterpret_cast<T**>(ptr)) = deletedItem; //<-----undefined behavior??
deletedItem = static_cast<T*>(ptr);
}
我想了解这个 class 在 C++17 标准下是否有未定义的行为。是否需要使用std::launder
?据我了解,这不是因为只涉及 void 指针和 T
指针。另外在deallocate
中对象已经被销毁了,所以应该是安全的。
您的代码在 every C++ 标准下展示 UB,甚至 C++20 允许(在某些情况下)隐式对象创建。但不是出于与 launder
.
有关的原因
你不能只是拿走一段记忆,假装那里有一个 T*
,然后把它当作一个存在。是的,即使对于指针这样的基本类型也是如此。在使用它们之前,您必须 创建 对象。因此,如果您有一块废弃的内存,并且想将 T*
推入其中,则需要使用 placement-new.
创建一个。
所以让我们重写您的代码(请注意,这可以编译,但未经测试;要点是您必须创建链表的元素):
template<class T>
class ObjectPool {
public:
ObjectPool( std::size_t cap );
virtual ~ObjectPool();
void* allocate() noexcept(false);
void deallocate( void* ptr );
private:
using empty_data = void*; //The data stored by an empty block. Does not point to a `T`.
using empty_ptr = empty_data*; //A pointer to an empty block.
static constexpr size_t entry_size = std::max(sizeof(T), sizeof(empty_data));
static constexpr std::align_val_t entry_align =
std::align_val_t(std::max(alignof(T), alignof(empty_data))); //Ensure proper alignment
void* pool;
std::mutex mutex;
std::size_t capacity;
//std::size_t counter; //We don't need a counter; the pool is empty if `freeItem` is NULL
empty_ptr freeItem; //Points to the first free item.
};
template<class T>
ObjectPool<T>::ObjectPool( std::size_t cap ) :
pool(::operator new(entry_size * cap, entry_align)),
capacity(cap)
{
//Build linked-list of free items, from back to front.
empty_data previous = nullptr; //Last entry points to nothing.
std::byte *byte_pool = reinterpret_cast<std::byte*>(pool); //Indexable pointer into memory
auto curr_ptr = &byte_pool[entry_size * capacity]; //Pointer to past-the-end element.
do
{
curr_ptr -= entry_size;
//Must *create* an `empty_data` in the storage.
empty_ptr curr = new(curr_ptr) empty_data(previous);
previous = empty_data(curr); //`previous` now points to the newly-created `empty_data`.
}
while(curr_ptr != byte_pool);
freeItem = empty_ptr(previous);
}
template<class T>
ObjectPool<T>::~ObjectPool()
{
::operator delete(pool, entry_size * capacity, entry_align);
}
template<class T>
void* ObjectPool<T>::allocate() noexcept(false) {
std::lock_guard<std::mutex> l(mutex);
if(!freeItem) { throw std::bad_alloc(); } //No free item means capacity full.
auto allocated = freeItem;
freeItem = empty_ptr(*freeItem); //Next entry in linked list is free or nullptr.
return allocated;
}
template<class T>
void ObjectPool<T>::deallocate( void* ptr ) {
std::lock_guard<std::mutex> l(mutex);
//Must *create* an `empty_data` in the storage. It points to the current free item.
auto newFree = new(ptr) empty_data(freeItem);
freeItem = newFree;
}
我创建了一个内存池,以便将其用作对象的新位置,并且我使用空闲槽来获得空闲列表以便重用这些槽:
template<class T>
class ObjectPool {
public:
ObjectPool( std::size_t cap );
virtual ~ObjectPool();
void* allocate() noexcept(false);
void deallocate( void* ptr );
private:
template<typename M>
static constexpr const M& max( const M& a, const M& b ) {
return a < b ? b : a;
}
using storage = typename std::aligned_storage<max(sizeof(T), sizeof(T*)), std::alignment_of<T>::value>::type;
storage* pool;
std::mutex mutex;
std::size_t capacity;
std::size_t counter;
T* deletedItem;
};
template<class T>
ObjectPool<T>::ObjectPool( std::size_t cap ) :
capacity(cap), counter(0), deletedItem(nullptr) {
if ( capacity > 0 )
pool = ::new storage[cap];
else
pool = nullptr;
}
template<class T>
ObjectPool<T>::~ObjectPool() {
::delete[] pool;
}
template<class T>
void* ObjectPool<T>::allocate() noexcept(false) {
std::lock_guard<std::mutex> l(mutex);
if ( deletedItem ) {
T* result = deletedItem;
deletedItem = *(reinterpret_cast<T**>(deletedItem)); //<-----undefined behavior??
return result;
}
if ( counter >= capacity ) {
throw std::bad_alloc();
}
return std::addressof(pool[counter++]);
}
template<class T>
void ObjectPool<T>::deallocate( void* ptr ) {
std::lock_guard<std::mutex> l(mutex);
*(reinterpret_cast<T**>(ptr)) = deletedItem; //<-----undefined behavior??
deletedItem = static_cast<T*>(ptr);
}
我想了解这个 class 在 C++17 标准下是否有未定义的行为。是否需要使用std::launder
?据我了解,这不是因为只涉及 void 指针和 T
指针。另外在deallocate
中对象已经被销毁了,所以应该是安全的。
您的代码在 every C++ 标准下展示 UB,甚至 C++20 允许(在某些情况下)隐式对象创建。但不是出于与 launder
.
你不能只是拿走一段记忆,假装那里有一个 T*
,然后把它当作一个存在。是的,即使对于指针这样的基本类型也是如此。在使用它们之前,您必须 创建 对象。因此,如果您有一块废弃的内存,并且想将 T*
推入其中,则需要使用 placement-new.
所以让我们重写您的代码(请注意,这可以编译,但未经测试;要点是您必须创建链表的元素):
template<class T>
class ObjectPool {
public:
ObjectPool( std::size_t cap );
virtual ~ObjectPool();
void* allocate() noexcept(false);
void deallocate( void* ptr );
private:
using empty_data = void*; //The data stored by an empty block. Does not point to a `T`.
using empty_ptr = empty_data*; //A pointer to an empty block.
static constexpr size_t entry_size = std::max(sizeof(T), sizeof(empty_data));
static constexpr std::align_val_t entry_align =
std::align_val_t(std::max(alignof(T), alignof(empty_data))); //Ensure proper alignment
void* pool;
std::mutex mutex;
std::size_t capacity;
//std::size_t counter; //We don't need a counter; the pool is empty if `freeItem` is NULL
empty_ptr freeItem; //Points to the first free item.
};
template<class T>
ObjectPool<T>::ObjectPool( std::size_t cap ) :
pool(::operator new(entry_size * cap, entry_align)),
capacity(cap)
{
//Build linked-list of free items, from back to front.
empty_data previous = nullptr; //Last entry points to nothing.
std::byte *byte_pool = reinterpret_cast<std::byte*>(pool); //Indexable pointer into memory
auto curr_ptr = &byte_pool[entry_size * capacity]; //Pointer to past-the-end element.
do
{
curr_ptr -= entry_size;
//Must *create* an `empty_data` in the storage.
empty_ptr curr = new(curr_ptr) empty_data(previous);
previous = empty_data(curr); //`previous` now points to the newly-created `empty_data`.
}
while(curr_ptr != byte_pool);
freeItem = empty_ptr(previous);
}
template<class T>
ObjectPool<T>::~ObjectPool()
{
::operator delete(pool, entry_size * capacity, entry_align);
}
template<class T>
void* ObjectPool<T>::allocate() noexcept(false) {
std::lock_guard<std::mutex> l(mutex);
if(!freeItem) { throw std::bad_alloc(); } //No free item means capacity full.
auto allocated = freeItem;
freeItem = empty_ptr(*freeItem); //Next entry in linked list is free or nullptr.
return allocated;
}
template<class T>
void ObjectPool<T>::deallocate( void* ptr ) {
std::lock_guard<std::mutex> l(mutex);
//Must *create* an `empty_data` in the storage. It points to the current free item.
auto newFree = new(ptr) empty_data(freeItem);
freeItem = newFree;
}