I made a single header object pool for use in personal projects. It's supposed to be easy to use, cross-platform and thread safe. It uses a free list for allocations and a hashmap for dereferencing handles. I'm self taught so I'm unsure if I'm performing any cardinal sins.
While coding it I focused on keeping my code short but legible, so I made use of modern (C++14/17) features. I try to follow CppCoreGuidelines everywhere but some things are too low level so they are in C-style C++ (e.g. memset
and memcpy
).
ObjectPool.hpp:
#pragma once
#ifndef OBJECT_POOL_PAGE_LENGTH
#define OBJECT_POOL_PAGE_LENGTH 32
#endif
#ifndef OBJECT_POOL_PAGE_ALIGNMENT
#define OBJECT_POOL_PAGE_ALIGNMENT 64
#endif
#define OBJECT_SIZE_1 sizeof(std::size_t)
#define OBJECT_SIZE_2 sizeof(std::size_t) * 2
#define OBJECT_SIZE_4 sizeof(std::size_t) * 4
#define OBJECT_SIZE_8 sizeof(std::size_t) * 8
#define OBJECT_SIZE_16 sizeof(std::size_t) * 16
#define OBJECT_SIZE_32 sizeof(std::size_t) * 32
#define OBJECT_SIZE_64 sizeof(std::size_t) * 64
#include <cstring>
#include <iostream>
#include <list>
#include <memory>
#include <mutex>
#include <sstream>
#include <tuple>
#include <typeinfo>
#include <unordered_map>
#include <vector>
/*! Things related to an aligned generic object pool implementation. */
namespace razaron::objectpool
{
/*! @cond */
using ArrayA = AlignedArray<char, OBJECT_POOL_PAGE_LENGTH * OBJECT_SIZE_2, OBJECT_POOL_PAGE_ALIGNMENT>;
using ArrayB = AlignedArray<char, OBJECT_POOL_PAGE_LENGTH * OBJECT_SIZE_4, OBJECT_POOL_PAGE_ALIGNMENT>;
using ArrayC = AlignedArray<char, OBJECT_POOL_PAGE_LENGTH * OBJECT_SIZE_8, OBJECT_POOL_PAGE_ALIGNMENT>;
using ArrayD = AlignedArray<char, OBJECT_POOL_PAGE_LENGTH * OBJECT_SIZE_16, OBJECT_POOL_PAGE_ALIGNMENT>;
using ArrayE = AlignedArray<char, OBJECT_POOL_PAGE_LENGTH * OBJECT_SIZE_32, OBJECT_POOL_PAGE_ALIGNMENT>;
using ArrayF = AlignedArray<char, OBJECT_POOL_PAGE_LENGTH * OBJECT_SIZE_64, OBJECT_POOL_PAGE_ALIGNMENT>;
using PoolA = std::tuple<Handle *, std::list<std::unique_ptr<ArrayA>>, std::shared_ptr<std::recursive_mutex>>;
using PoolB = std::tuple<Handle *, std::list<std::unique_ptr<ArrayB>>, std::shared_ptr<std::recursive_mutex>>;
using PoolC = std::tuple<Handle *, std::list<std::unique_ptr<ArrayC>>, std::shared_ptr<std::recursive_mutex>>;
using PoolD = std::tuple<Handle *, std::list<std::unique_ptr<ArrayD>>, std::shared_ptr<std::recursive_mutex>>;
using PoolE = std::tuple<Handle *, std::list<std::unique_ptr<ArrayE>>, std::shared_ptr<std::recursive_mutex>>;
using PoolF = std::tuple<Handle *, std::list<std::unique_ptr<ArrayF>>, std::shared_ptr<std::recursive_mutex>>;
using PoolTuple = std::tuple<PoolA, PoolB, PoolC, PoolD, PoolE, PoolF>;
template <typename Pool>
using Page = typename std::tuple_element<1, Pool>::type::value_type::element_type;
// clang-format off
template <typename T>
using PoolCond1 = std::conditional <sizeof(T) <= OBJECT_SIZE_2, PoolA,
typename std::conditional <sizeof(T) <= OBJECT_SIZE_4, PoolB,
typename std::conditional <sizeof(T) <= OBJECT_SIZE_8, PoolC,
typename std::conditional <sizeof(T) <= OBJECT_SIZE_16, PoolD,
typename std::conditional <sizeof(T) <= OBJECT_SIZE_32, PoolE, PoolF>::type>::type>::type>::type>;
// clang-format on
/*! @endcond */
/*! Hashmap for mapping Handle%s to pointers. */
using HandleMap = std::unordered_map<Handle, void *, HandleHash, HandleEqual>;
/*! Stores objects of any type with size upto \c sizeof(std::size_t)*64 Bytes in contiguous aligned memory.
* For more information and examples, see page \ref objectpool.
*/
class ObjectPool
{
public:
ObjectPool() noexcept; /*!< Default constructor. */
template <std::size_t... Is>
void init(PoolTuple &p);
/*! Copies an object of type T into the ObjectPool.
*
* @tparam T The type of the object to be moved int o the ObjectPool.
*
* @param object The object to copy into the ObjectPool.
*
* @exception std::length_error T is too large for ObjectPool.
*
* @retval Handle On success, a Handle for accessing the object.
* @retval Handle On failure, an empty Handle.
*/
template <class T>
Handle push(const T &object);
/*! Moves an object of type T into the ObjectPool.
*
* @tparam T The type of the object to be moved int o the ObjectPool.
*
* @param object The object to move into the ObjectPool.
*
* @exception std::length_error T is too large for ObjectPool.
*
* @retval Handle On success, a Handle for accessing the object.
* @retval Handle On failure, an empty Handle.
*/
template <class T>
Handle push(T &&object);
/*! Constructs an object of type T directly into the ObjectPool.
*
* @tparam T The type of the object to be moved into the ObjectPool.
* @tparam Args The parameter pack used to construct the T object.<sup>[1]</sup>
*
* @param args Constructor arguments to pass to the constructor of T.
*
* @exception std::length_error T is too large for ObjectPool.
*
* @retval Handle On success, a Handle for accessing the object.
* @retval Handle On failure, an empty Handle.
*
* <small><sup>[1]</sup> Don't enter this. It <a title="cppreference" href="http://en.cppreference.com/w/cpp/language/template_argument_deduction">deduced</a> by the compiler.</small>
*/
template <class T, class... Args>
Handle emplace(Args... args);
/*! Gets a pointer to an object in the ObjectPool.
*
* @tparam T The type of the object to get from the ObjectPool.
*
* @param handle The Handle used to search for the object in the ObjectPool.
*
* @exception std::invalid_argument T and handle are mismatched.
* @exception std::length_error T is too large for ObjectPool.
*
* @retval T* On success, a pointer to the desired object.
* @retval nullptr On failure, a nullptr.
*/
template <class T>
T *get(const Handle &handle);
//TODO template<class T> std::vector<T*> get(std::vector<Handle> handles);
/*! Removes an object from the ObjectPool and free's the space for reuse.
* It calls the destructor for non-trivially destructible objects.
*
* @tparam T The type of the object to remove from the ObjectPool.
*
* @param handle The Handle of the object to remove from the ObjectPool.
*/
template <class T>
void erase(const Handle &handle);
/*! Moves an object to an earlier free position.
*
* @tparam T The type of the object to reorder.
*
* @param handle The Handle of the object to reorder
*
* @retval Handle On success, a Handle for the objects new position.
* @retval Handle On failure, an empty Handle.
*/
template <class T>
Handle reorder(const Handle &handle);
/*! Removes unused pages, releasing their memory. */
void shrink();
/*! Returns the current total capacity in bytes. */
std::size_t capacity(); // add overload with size parameter. Checks how many size bytes long object can fit.
private:
template <class T, class Pool, class... Args>
Handle allocateConstruct(Args... args);
template <class T, class Pool>
Handle allocateMove(T &&object);
template <class Pool>
void addPage();
template <class Pool>
Page<Pool>* getPage(HandleIndex index);
template <class T>
T *getObject(const Handle &handle);
template <class T, class Pool>
T *getPointer(const Handle &handle);
template <class Pool, typename T>
typename std::enable_if<std::is_pointer<T>::value, HandleIndex>::type getIndex(T ptr);
template <class T, class Pool>
void erase(const Handle &handle);
template <class Pool>
void shrink();
PoolTuple _pools;
HandleMap _hashMap;
std::mutex _hashMapMutex;
};
/* *************************************************
PUBLIC FUNCTIONS
****************************************************/
template <std::size_t... Is>
void ObjectPool::init(PoolTuple &p)
{
((std::get<2>(std::get<Is>(p)) = std::make_shared<std::recursive_mutex>()), ...);
}
inline ObjectPool::ObjectPool() noexcept
: _hashMap{}, _hashMapMutex{}
{
init<0, 1, 2, 3, 4, 5>(_pools);
}
template <class T>
inline Handle ObjectPool::push(const T &object)
{
// Find the pool that fits T
using Pool = typename PoolCond1<T>::type;
T val = object;
if (sizeof(T) <= OBJECT_SIZE_64)
{
return allocateMove<T, Pool>(std::move(val));
}
else
{
std::stringstream message;
message << typeid(T).name() << " is too large for ObjectPool. sizeof(" << typeid(T).name() << "): "
<< ".";
throw std::length_error(message.str());
}
}
template <class T>
inline Handle ObjectPool::push(T &&object)
{
// Find the pool that fits T
using Pool = typename PoolCond1<T>::type;
if (sizeof(T) <= OBJECT_SIZE_64)
{
return allocateMove<T, Pool>(std::forward<T>(object));
}
else
{
std::stringstream message;
message << typeid(T).name() << " is too large for ObjectPool. sizeof(" << typeid(T).name() << "): "
<< ".";
throw std::length_error(message.str());
}
}
template <class T, class... Args>
inline Handle ObjectPool::emplace(Args... args)
{
// Find the pool that fits T
using Pool = typename PoolCond1<T>::type;
if (sizeof(T) <= OBJECT_SIZE_64)
{
return allocateConstruct<T, Pool>(args...);
}
else
{
std::stringstream message;
message << typeid(T).name() << " is too large for ObjectPool. sizeof(" << typeid(T).name() << "): " << sizeof(T) << ".";
throw std::length_error(message.str());
}
}
template <class T>
inline T *ObjectPool::get(const Handle &handle)
{
if (handle.size != sizeof(T))
{
std::stringstream message;
message << "Type mismatch. HandleSize: " << handle.size << " != sizeof(T): " << sizeof(T) << ". typeid(T): " << typeid(T).name();
throw std::invalid_argument(message.str());
}
else if (sizeof(T) <= OBJECT_SIZE_64)
{
return getObject<T>(handle);
}
else
{
std::stringstream message;
message << "HandleSize (" << handle.size << ") too large for ObjectPool.";
throw std::length_error(message.str());
}
}
template <class T>
inline void ObjectPool::erase(const Handle &handle)
{
// Find the pool that fits T
using Pool = typename PoolCond1<T>::type;
if (handle.size != sizeof(T))
{
std::stringstream message;
message << "Type mismatch. HandleSize: " << handle.size << " != sizeof(T): " << sizeof(T) << ". typeid(T): " << typeid(T).name();
throw std::invalid_argument(message.str());
}
else if (sizeof(T) <= OBJECT_SIZE_64)
{
return erase<T, Pool>(handle);
}
else
{
std::stringstream message;
message << "HandleSize (" << handle.size << ") too large for ObjectPool.";
throw std::length_error(message.str());
}
}
template <class T>
inline Handle ObjectPool::reorder(const Handle &handle)
{
using Pool = typename PoolCond1<T>::type;
if (handle.size != sizeof(T))
{
std::stringstream message;
message << "Type mismatch. HandleSize: " << handle.size << " != sizeof(T): " << sizeof(T) << ". typeid(T): " << typeid(T).name();
throw std::invalid_argument(message.str());
}
auto pool = &std::get<Pool>(_pools);
std::lock_guard<std::recursive_mutex> lk{ *std::get<2>(*pool) };
// If the first free pointer is located after handle, return null
if (handle.index < getIndex<Pool>(std::get<0>(*pool)))
return {};
T temp;
// If no object currently exists for handle, returm null
if (getObject<T>(handle))
temp = *getObject<T>(handle);
else
return {};
erase<T, Pool>(handle);
return allocateMove<T, Pool>(std::move(temp));
}
inline std::size_t ObjectPool::capacity()
{
auto &pA = std::get<PoolA>(_pools);
auto &pB = std::get<PoolB>(_pools);
auto &pC = std::get<PoolC>(_pools);
auto &pD = std::get<PoolD>(_pools);
auto &pE = std::get<PoolE>(_pools);
auto &pF = std::get<PoolF>(_pools);
return std::get<1>(pA).size() * sizeof(ArrayA) + std::get<1>(pB).size() * sizeof(ArrayB) + std::get<1>(pC).size() * sizeof(ArrayC) + std::get<1>(pD).size() * sizeof(ArrayD) + std::get<1>(pE).size() * sizeof(ArrayE) + std::get<1>(pF).size() * sizeof(ArrayF);
}
inline void ObjectPool::shrink()
{
shrink<PoolA>();
shrink<PoolB>();
shrink<PoolC>();
shrink<PoolD>();
shrink<PoolE>();
shrink<PoolF>();
}
/* *************************************************
PRIVATE FUNCTIONS
****************************************************/
template <class T, class Pool, class... Args>
inline Handle ObjectPool::allocateConstruct(Args... args)
{
return allocateMove<T, Pool>(T{ args... });
}
template <class T, class Pool>
inline Handle ObjectPool::allocateMove(T &&object)
{
auto pool = &std::get<Pool>(_pools);
std::lock_guard<std::recursive_mutex> lk{ *std::get<2>(*pool) };
// If the next free position pointer points to non-existant page, add a new page
size_t totalPositions = std::get<1>(*pool).size() * OBJECT_POOL_PAGE_LENGTH;
if (totalPositions == 0 || totalPositions <= std::get<0>(*pool)->index)
{
addPage<Pool>();
}
// Get pointers to the current and next free elements
Handle *curFree = std::get<0>(*pool);
Handle *nextFree = getPointer<Handle, Pool>(*curFree);
// Copy object data to the location current free pointer
std::memcpy(curFree, &object, sizeof(T));
// Set the pools first free pointer to the next free pointer
std::get<0>(*pool) = nextFree;
// Configure a Handle for the newly placed object
Handle h{ HandleSize{ sizeof(T) }, HandleIndex{ getIndex<Pool>(curFree) }, false };
// Adds the new object to the ObjectPools hashmap
{
std::lock_guard<std::mutex> lk{ _hashMapMutex };
_hashMap[h] = static_cast<void *>(curFree);
}
return h;
}
template <class Pool>
inline void ObjectPool::addPage()
{
auto pool = &std::get<Pool>(_pools);
// Create and push a new page onto the pool
auto page = new Page<Pool>;
std::get<1>(*pool).emplace_back(page);
// Initialize the pages positions with free handles pointing to the next free Handle
auto pageData = std::get<1>(*pool).back()->data();
for (auto i = 0; i < OBJECT_POOL_PAGE_LENGTH; i++)
{
HandleIndex nextFree = static_cast<HandleIndex>(i + 1 + ((std::get<1>(*pool).size() - 1) * OBJECT_POOL_PAGE_LENGTH));
Handle h = { static_cast<HandleSize>(page->size() / OBJECT_POOL_PAGE_LENGTH), nextFree, true };
std::memcpy(&pageData[i * page->size() / OBJECT_POOL_PAGE_LENGTH], &h, sizeof(h));
}
// If it's the first page, set the first free position to the beginning of the page
if (std::get<0>(*pool) == nullptr)
std::get<0>(*pool) = reinterpret_cast<Handle *>(page->data());
}
template <class Pool>
inline typename std::tuple_element<1, Pool>::type::value_type::element_type* ObjectPool::getPage(HandleIndex index)
{
auto pool = &std::get<Pool>(_pools);
// Quotient is the page number and remainder is the position in that page
std::div_t d = std::div(index, OBJECT_POOL_PAGE_LENGTH);
// Finds a pointer to the correct page
Page<Pool> *page = nullptr;
for (auto &p : std::get<1>(*pool))
{
if (!d.quot)
{
page = p.get();
break;
}
d.quot--;
}
return page;
}
template <class T>
inline T *ObjectPool::getObject(const Handle &handle)
{
std::lock_guard<std::mutex> lk{ _hashMapMutex };
auto it = _hashMap.find(handle);
if (it != _hashMap.end())
{
return static_cast<T *>(it->second);
}
else
return nullptr;
}
template <class T, class Pool>
inline T *ObjectPool::getPointer(const Handle &handle)
{
auto pool = &std::get<Pool>(_pools);
std::lock_guard<std::recursive_mutex> lk{ *std::get<2>(*pool) };
// Find the page containg handle
auto page = getPage<Pool>(handle.index);
// Quotient is the page number and remainder is the position in that page
std::div_t d = std::div(handle.index, OBJECT_POOL_PAGE_LENGTH);
// Find and cast the element refering to objects first byte
auto objectPtr = reinterpret_cast<T *>(&page->data()[d.rem * std::get<0>(*pool)->size]);
return objectPtr;
}
template <class Pool, typename T>
inline typename std::enable_if<std::is_pointer<T>::value, HandleIndex>::type ObjectPool::getIndex(T ptr)
{
auto pool = &std::get<Pool>(_pools);
// Find the page that contains ptr
std::size_t ptrAdr = reinterpret_cast<std::size_t>(ptr);
std::size_t pageAdr = 0;
std::size_t diff = 0;
int pageNumber = 0;
for (auto &p : std::get<1>(*pool))
{
pageAdr = reinterpret_cast<std::size_t>(p->data());
diff = ptrAdr - pageAdr;
++pageNumber;
if (diff >= 0 && diff < sizeof(Page<Pool>))
break;
}
// Throw if no page found
if (!(diff >= 0 && diff < sizeof(Page<Pool>)))
{
throw std::out_of_range("Pointer is not in any page.");
}
// Calculate index relative to it's page
std::size_t position = ptrAdr - pageAdr;
position = position / std::get<0>(*pool)->size;
// Add add sum of preceding positions to get absolute index
position = position + (pageNumber - 1) * OBJECT_POOL_PAGE_LENGTH;
// If position is in valid range, return. Else, throw.
if (position <= std::numeric_limits<HandleIndex>::max())
{
return static_cast<HandleIndex>(position);
}
else
{
std::stringstream message;
message << "Calculated position too large for HandleIndex max value. std::numeric_limits<HandleIndex>::max()" << std::numeric_limits<HandleIndex>::max();
throw std::overflow_error(message.str());
}
}
template <class T, class Pool>
inline void ObjectPool::erase(const Handle &handle)
{
auto pool = &std::get<Pool>(_pools);
std::lock_guard<std::recursive_mutex> lk{ *std::get<2>(*pool) };
// Get index of first free position
auto posCurFree = getIndex<Pool>(std::get<0>(*pool));
// Fail if first free position and object being removed are the same
if (handle.index == posCurFree) return;
Handle *ptrToRemove = getObject<Handle>(handle);
// Call object destructor if it is manually set
if (std::is_destructible<T>::value && !std::is_trivially_destructible<T>::value)
reinterpret_cast<T *>(ptrToRemove)->~T();
// Resets the data back to zero
std::memset(ptrToRemove, 0, std::get<0>(*pool)->size);
// If the object being removed is located BEFORE the first free position
if (handle.index < posCurFree)
{
// Setup the object being removed to become the next firstFree pointer
ptrToRemove->isFree = true;
ptrToRemove->size = std::get<0>(*pool)->size;
ptrToRemove->index = posCurFree;
std::get<0>(*pool) = ptrToRemove;
}
// If the object being removed is located AFTER the first free position
else
{
Handle *ptrPrevFree = nullptr;
Handle *ptrNextFree = std::get<0>(*pool);
std::size_t posNextFree = getIndex<Pool>(ptrNextFree);
// Loop through free positions until handle is inbetween prevFree and nextFree
while (posNextFree < handle.index)
{
ptrPrevFree = ptrNextFree;
ptrNextFree = getPointer<Handle, Pool>(*ptrNextFree);
posNextFree = getIndex<Pool>(ptrNextFree);
}
// Currently, ptrToRemove is zeroed, so I have to get it's index from handle
ptrPrevFree->index = handle.index;
// Setup the ptr being removed to be inbetween ptrPrevFree and ptrNextFree
ptrToRemove->isFree = true;
ptrToRemove->size = std::get<0>(*pool)->size;
ptrToRemove->index = static_cast<HandleIndex>(posNextFree);
}
// Removes object from the hashmap.
{
std::lock_guard<std::mutex> lk{ _hashMapMutex };
if (!_hashMap.erase(handle))
{
std::stringstream message;
message << "Handle{ size: " << handle.size << ", index: " << handle.index << " }"
<< " not found in ObjectPool::_hashMap.";
throw std::out_of_range(message.str());
}
}
return;
}
template <class Pool>
inline void ObjectPool::shrink()
{
auto pool = &std::get<Pool>(_pools);
std::lock_guard<std::recursive_mutex> lk{ *std::get<2>(*pool) };
auto pages = &std::get<1>(*pool);
if (!std::get<0>(*pool))
return;
std::vector<Handle *> freePtrs{ std::get<0>(*pool) };
std::size_t lastPos = pages->size() * OBJECT_POOL_PAGE_LENGTH;
// loop through all free handles
while (freePtrs.back()->index != lastPos)
{
freePtrs.push_back(getPointer<Handle, Pool>(*freePtrs.back()));
}
if (freePtrs.size() < OBJECT_POOL_PAGE_LENGTH)
return;
lastPos++;
size_t pos = freePtrs.size();
size_t toDelete = 0;
while (pos > 0)
{
pos -= OBJECT_POOL_PAGE_LENGTH;
if (freePtrs[pos]->index == (lastPos -= OBJECT_POOL_PAGE_LENGTH))
toDelete++;
else
break;
}
auto begin = pages->begin();
auto end = pages->end()--;
std::advance(begin, pages->size() - toDelete);
pages->erase(begin, end);
}
}
Handle
is a free list handle struct. It's always smaller than sizeof(2*std::size_t)
(outside of exotic systems). This is not for review.
// Handling for pointers etc.
using HandleSize = std::size_t; /*!< Represents the size of Handle%d objects. */
using HandleIndex = unsigned short; /*!< Represents the indexed location of Handle%d objects. */
/*! Handles are used to abstract data access away from pointers. */
struct Handle
{
HandleSize size{}; /*!< The size of the Handle%d object. */
HandleIndex index{};/*!< The indexed location of the Handle%d object. */
bool isFree{true};/*!< Whether the index denotes a free or occupied location. */
/*! Basic equality comparator. */
bool operator==(const Handle &rhs) noexcept
{
return (size == rhs.size && index == rhs.index && isFree == rhs.isFree);
}
};
struct HandleHash
{
std::size_t operator()(const Handle &h) const noexcept
{
auto hash1 = std::hash<HandleSize>()(h.size);
auto hash2 = std::hash<HandleIndex>()(h.index);
return hash1 ^= hash2 + 0x9e3779b9 + (hash1 << 6) + (hash1 >> 2);
}
};
struct HandleEqual
{
bool operator()(const Handle &lhs, const Handle &rhs) const noexcept
{
return lhs.size == rhs.size && lhs.index == rhs.index;
}
};
AlignedArray
is nothing more than a wrapper around a c-style array allocated in aligned heap memory. This isn't for review.
inline void* aligned_malloc(size_t size, size_t align) noexcept
{
void *result;
#ifdef _WIN32
result = _aligned_malloc(size, align);
#else
if(posix_memalign(&result, align, size)) result = 0;
#endif
return result;
}
inline void aligned_free(void *ptr) noexcept
{
#ifdef _WIN32
_aligned_free(ptr);
#else
free(ptr);
#endif
}
template <class T, std::size_t S, std::size_t A>
struct alignas(A) AlignedArray
{
public:
T* data() noexcept { return _array.data(); }
std::size_t size() noexcept { return S; }
std::size_t alignment() noexcept { return A; }
T& operator [](std::size_t i) { return _array[i]; }
void* operator new(std::size_t sz) { return aligned_malloc(sz, A); }
void operator delete(void* ptr) { return aligned_free(ptr); }
private:
std::array<T, S> _array{};
};
Example usage:
struct Foo
{
float x, y;
Foo(float x, float y) : x{ x }, y{ y } {}
~Foo() { std::clog << "Destroyed!" << std::endl; }
};
using Bar = std::array<char, 3>;
ObjectPool pool;
Handle fooHdl = pool.emplace<Foo>(3.14159f, 2.71828f); // In place construction
Handle barHdl = pool.push<Bar>(Bar{"GB"}); // Passing a temporary/rvalue
auto fooPtr = pool.get<Foo>(fooHdl); // auto resolves to Foo*
float sum = fooPtr->x + fooPtr->y;
auto barPtr = pool.get<Bar>(barHdl); // auto resolves to Bar*
std::cout << barPtr->data() << std::endl;
pool.erase<Foo>(fooHdl); // Calls Foo::~Foo then deallocates from pool
pool.erase<Bar>(barHdl); // Just deallocates from pool
Throw the Handle
and AlignedArray
code into the ObjectPool.hpp
file and the example into a main.cpp
file and it should compile. Remember to use the compiler options -std=c++17 -lpthread
or equivalent.
The program should output:
destroy
GB
destroy
destroy
showing up twice is normal. Even std::vector<Foo>::push_back(Foo&&)
would output that.
I would like feedback on a few things:
- Style: I have a code style that I try to be consistent with. Am I doing anything fundamentally bad?
- Preprocessor: I'm banal about type safety so avoid using it in all my other code. Is it better to switch the macros over over to
constexpres
for added type safety? - Code: I'd like an overall review but also have specific questions.
- Am I making proper use of modern C++?
- Is the code easy to understand?
- Am I handling thread safety properly?
- Would you use this over alternatives? I made it just for personal use but designed it to be stranger friendly. Did I accomplish that?
Things I'm aware of but don't mind critique:
- I can replace all the
ArrayX
andPoolX
typedefs with a single template each. The code is already quite heavily templated so I find the current way easier to read. Also it's friendlier on intellisense/linters. ObjectPool::capacity()
andObjectPool::shrink()
can use fold expressions. I only noticed that as I was writing this question...- I'm using pointer arithmetic. CppCoreGuidelines says no. I will refactor with
gsl::span
. - I use exceptions (divisive subject). In all my personal code I use exceptions only at the lowest level. The intent of my exceptions is that, if caught, you should exit cleanly and not try to carry on after the stack unwind. I basically use them as a runtime alternative to things I can't (or don't want to) check at compile time. To that end I usually provide helper functions so
try
/catch
blocks are only for debugging/error checking. - Spacing is brokeded due to cross-platform/editor development and forgetting to set the tab to space feature in editors after reinstalling Windows/Linux a few times. Plan to fix.
ObjectPool::init()
is just a helper so should beprivate
but I want it near the constructor for prettiness. Will be replacing with a templated lambda inside the constructor come C++20.- It's not nice to have to supply a template argument to the public functions but, just like for
std::tuple
, it's needed for type safety.
PS. This is my first time using codereview.stackexchange. So I apologise in advance if the question is ill-formed.