writing a generic object pool in C++ For a hobby project, My aim is to design and implement a generic system for pooling an arbitrary class of object. I tried implementing it using templates.
This pool should support 4 actions:
&quot;Create a pool&quot;
&quot;Allocate an object from this pool&quot;
&quot;Deallocate an object from this pool&quot;
&quot;Destroy this pool&quot;
For sake of simplicity, there is no multi-threading involved.

If I have to pool classes like this example, how do I do that?

typedef char            ByteType;
typedef void*           PointerType;
typedef char            FixedStringType[256];

// A basic struct
struct Point
{
    int x, y, z;
};


Can someone please provide your thoughts/feedback on bugs and how to improve this code?

//ObjectPool.h
#pragma once
#include &lt;vector&gt;
#include &lt;iostream&gt;

const int g_MaxNumberOfObjectsInPool = 2;

template&lt;typename T&gt;
class DefaultAllocator {
public:
    T* operator()() {
        return new T{};
    }
    void operator()(T* p) {
        delete p;
    }
    void reset() {
        std::cout &lt;&lt; &quot;reset function called from default allocator&quot; &lt;&lt; std::endl;
    }
};

template&lt;typename T, typename AllocatorT=DefaultAllocator&lt;T&gt;&gt;
class ObjectPool
{
    struct ObjectInfo {
        bool isInUse{};
        T* ptrObject{};
    };
    static inline std::vector&lt;ObjectInfo&gt; poolObjects{};
    static inline AllocatorT allocator{};
public:
    static T* getObject() {
        for (auto&amp; currObj : poolObjects) {
            if (!currObj.isInUse) {
                currObj.isInUse = true;
                std::cout &lt;&lt; &quot;Existing Object returned&quot; &lt;&lt; std::endl;
                return currObj.ptrObject;
            }
        }

        if (poolObjects.size() == g_MaxNumberOfObjectsInPool) {
            std::cout &lt;&lt; &quot;Pool is full &quot; &lt;&lt; std::endl;
            return nullptr;
        }

        std::cout &lt;&lt; &quot;Creating a new object&quot; &lt;&lt; std::endl;
        //auto newObj = new T{};
        auto newObj = allocator();
        poolObjects.push_back({ true, newObj });        
        return newObj;
    }

    static void releaseObject(T* ptrSo) {
        for (auto&amp; currObj : poolObjects) {
            if (currObj.ptrObject == ptrSo) {
                currObj.isInUse = false;
                return;
            }
        }
    }

    static void destroy() {
        for (auto&amp; currObj : poolObjects) {
            if (currObj.isInUse) {
                std::cout &lt;&lt; &quot;WARNING! this object is still in use&quot; &lt;&lt; std::endl;
            }
            allocator(currObj.ptrObject);
        }
        allocator.reset();
        poolObjects.clear();
    }
};

The main file looks like this:
#include &quot;ObjectPool.h&quot;
#include &lt;iostream&gt;

class PrivateClass {
    PrivateClass() {

    }
public:
    void func() {

    }
    friend class PrivateAllocator;
};

class PrivateAllocator {
public:
    PrivateClass* operator()() {
        return new PrivateClass{};
    }
    void operator()(PrivateClass* p) {
        delete p;
    }
    void reset() {
        std::cout &lt;&lt; &quot;reset function called&quot; &lt;&lt; std::endl;
    }
};

int main() {
    using integer = ObjectPool&lt;int&gt;;
    auto int1 = integer::getObject();
    auto int2 = integer::getObject();
    auto int3 = integer::getObject();

    integer::releaseObject(int1);
    auto int4 = integer::getObject();
    integer::destroy();
    return 0;
}
#pragma once

Don’t use #pragma once. It’s non-standard, it breaks silently and unexpectedly and when it does all hell breaks loose, and the standard solution (include guards) are just as efficient in practice.
const int g_MaxNumberOfObjectsInPool = 2;

There doesn’t seem any good reason why this should be a global variable. It would be better as a static data member of ObjectPool.
It should probably also be constexpr.
But the most important issue is that you are using (signed) int for a size type. That will cause problems if you’re not very careful, because most sizes in C++ use unsigned integers. Yes, this was a mistake, but you can’t fix it by stubbornly ignoring it and insisting on using signed types for sizes instead. “When in Rome” and all.
The correct type to use here is probably std::size_t.
template&lt;typename T&gt;
class DefaultAllocator {
public:
    T* operator()() {
        return new T{};
    }
    void operator()(T* p) {
        delete p;
    }
    void reset() {
        std::cout &lt;&lt; &quot;reset function called from default allocator&quot; &lt;&lt; std::endl;
    }
};

… why?
There is a standard allocator interface in C++, and it already does everything you need it to do (and more! it’s actually possible to build what is functionally an object pool using any container type(s) with the right allocator… and the right allocator comes built-in with the standard). It makes zero sense to just ignore that and reinvent your own, incompatible allocator interface. (Especially one with a nonsensical, hobbled interface. What does reset() even do?)
Not to mention that you’re already using the standard allocator interface anyway! std::vector&lt;T&gt; is actually std::vector&lt;T, std::allocator&lt;T&gt;&gt;. So when you do:
auto newObj = allocator();
poolObjects.push_back({ true, newObj }); 

You are using two allocators: your allocator in the first line, and the vector’s standard allocator in the second.
All-in-all, making your own, incompatible allocator regime when a perfectly good standard regime exists (and you even use it!) just seems remarkably silly. And it will get worse….
class ObjectPool

You have made everything in this class static, presumably to hack a singleton-like interface. That’s a bad idea for a number of reasons.
First, singletons are an anti-pattern. In rare circumstances, they are genuinely useful, but even then it should be opt-in… not forced by the type… because that makes testing extremely difficult. And you are testing your code, aren’t you? Hm?
In addition to making testing difficult, forcing a singleon design makes your object pool less useful. What if I want multiple object pools? What if, for example, I’m making a engineering modelling program, and I want to put all the points for the current model in a pool… but then I want to allow a split-screen view of two models side-by-side, so I now need two pools? Why would you punish users of your object pool by denying them the ability to create more than one? What’s the sense in that?
If you made your object pool in the normal, sensible way—that is, not making everything static—then if you really wanted a singleton object pool later, you could always make a trivial wrapper:
template &lt;typename T, typename Allocator&gt;
class SingletonObjectPool
{
    static ObjectPool&lt;T, Allocator&gt; _pool;

public:
    static auto getObject() { return _pool.getObject(); }

    static auto releaseObject(T* p) { return _pool.releaseObject(); }

    static auto destroy() { return _pool.destroy(); }
};

But you can’t go the other way; if you make ObjectPool with everything static, you can’t make a non-static object pool.
struct ObjectInfo {
    bool isInUse{};
    T* ptrObject{};
};
static inline std::vector&lt;ObjectInfo&gt; poolObjects{};

This is really kinda defeating the whole purpose of std::vector.
First of all, ObjectInfo is a dangerous type, because that T* in there is an owning pointer… but it’s not a smart pointer. For that sin alone I’d refuse the whole thing from any project I was in charge of.
The result of this sin is that your object pool leaks all kinds of memory if you don’t manually call destroy()… which not only defeats the purpose of std::vector, it defeats the whole purpose of C++!
The standard first fix to this kind of problem is to replace pointers with the objects themselves; C++ is a value-semantic language, after all:
struct ObjectInfo
{
    bool isInUse = false;
    T object;
};

But if you do this, then you run into problems due to the fact that you’re using a std::vector as your actual pool. Every time you add a new object, it may trigger an internal reallocation of the vector’s array, which would change the addresses of all the objects… and you’re using object addresses as handles, so it would completely break your object pool.
There are two options to fix this problem: don’t use vector as your pool, or don’t store the pooled objects directly in the vector. Your solution is option 2, which, with smart pointers gives:
struct ObjectInfo
{
    bool isInUse = false;
    std::unique_ptr&lt;T&gt; ptrObject = nullptr;
};

Is that the best solution? Eeeh. But we’ll get back to that.
static T* getObject() {
    for (auto&amp; currObj : poolObjects) {
        if (!currObj.isInUse) {
            currObj.isInUse = true;
            std::cout &lt;&lt; &quot;Existing Object returned&quot; &lt;&lt; std::endl;
            return currObj.ptrObject;
        }
    }

    if (poolObjects.size() == g_MaxNumberOfObjectsInPool) {
        std::cout &lt;&lt; &quot;Pool is full &quot; &lt;&lt; std::endl;
        return nullptr;
    }

    std::cout &lt;&lt; &quot;Creating a new object&quot; &lt;&lt; std::endl;
    //auto newObj = new T{};
    auto newObj = allocator();
    poolObjects.push_back({ true, newObj });        
    return newObj;
}

This function is way too complex. It does way too much work for a single function:

First it finds the first not-in-use object, if any.
Then it checks whether the pool is full.
Finally, it creates a new object in the pool.

Each one of those steps could be its own function. And some of them could even be public functions, because they might be useful to users. For example, the first step, finding the first not-in-use object could be handy for an algorithm that would like a T, but if it can’t get one, can do something else instead.
So you might do:
template&lt;typename T, typename Allocator&gt;
class ObjectPool
{
    // ... [snip] ...

public:
    // tries to get an available object, and if that fails, tries to create one
    auto getObject() -&gt; T*
    {
        if (auto p = getAvailableObject(); p)
            return p;

        return newObject();
    }

    // gets an object only if one is available, never creates one, never fails
    auto getAvailableObject() noexcept -&gt; T*
    {
        if (auto p_info = _findAvailableObjectInfo(); p_info)
        {
            p_info-&gt;isInUse = true;
            std::cout &lt;&lt; &quot;Existing Object returned\n&quot;;
            return (p_info-&gt;ptrObject).get();
        }
        else
        {
            return nullptr;
        }
    }

    // always creates an object (unless the pool is full)
    auto newObject() -&gt; T*
    {
        if (poolObjects.size() == g_MaxNumberOfObjectsInPool)
            throw std::runtime_error{&quot;pool is full&quot;};

        std::cout &lt;&lt; &quot;Creating a new object\n&quot;;
        
        auto newObj = std::unique_ptr{allocator()};
        poolObjects.emplace_back(true, std::move(newObj));
        return newObj;
    }

private:
    auto _findAvailableObjectInfo() noexcept -&gt; ObjectInfo*
    {
        auto is_in_use = [](auto&amp;&amp; p_info) { return p_info-&gt;isInUse; };

        if (auto p = std::find_if_not(poolObjects.begin(), poolObjects.end(), is_in_use); p != poolObjects.end())
            return *p;
        else
            return nullptr;
    }

    // ... [snip] ...
};

Some additional notes:

Don’t write naked loops. Use standard algorithms. In this case, that loop is obviously a std::find_if(_not)().
Don’t use std::endl. Every time you use it, it flushes the output stream, which can slow things down tremendously.
Don’t return nullptr when an object can’t be gotten. That just gives users of the object pool headaches, because now they have to check every single call to getObject(). If the point is to get an object, and an object can’t be gotten, then throw an exception to indicate failure. (OR, if you really want to avoid relying on exceptions, look in to an “expected” type, like the proposed std::expected, or boost::outcome.)

static void releaseObject(T* ptrSo) {
    for (auto&amp; currObj : poolObjects) {
        if (currObj.ptrObject == ptrSo) {
            currObj.isInUse = false;
            return;
        }
    }
}

This loop is another std::find_if().
static void destroy() {
    for (auto&amp; currObj : poolObjects) {
        if (currObj.isInUse) {
            std::cout &lt;&lt; &quot;WARNING! this object is still in use&quot; &lt;&lt; std::endl;
        }
        allocator(currObj.ptrObject);
    }
    allocator.reset();
    poolObjects.clear();
}

There is no single standard algorithm that could cleanly replace this loop. Part of the problem is that it does multiple things… which is bad. You could replace it with a single std::for_each(), of course, but it might make more sense to break it into two steps—one to check for still-in-use, and one to do the deallocation.
This becomes especially important if you use smart pointers, in which case the deallocation step becomes completely unnecessary. And, given that allocator.reset() seems to serve no purpose, the whole thing could boil down to:
static void destroy()
{
    auto is_in_use = [](auto&amp;&amp; p_info) { return p_info-&gt;isInUse; };

    std::for_each(
        std::remove_if(poolObjects.begin(), poolObjects.end(), is_in_use),
        poolObjects.end(),
        [] (auto&amp;&amp;) { std::cout &lt;&lt; &quot;WARNING! this object is still in use\n&quot;; }
    );

    poolObjects.clear();
}

But frankly, a destroy() function is silly in C++. We have built-in destroy() functions; they’re called destructors, and they’re awesome precisely because you don’t have to call them… they work automatically. If the object pool weren’t a hacky-singleton due to everything being static, then the whole function (except for the warning about unreleased objects part) would become obsolete due to the destructor.
An alternative design
Let me propose an alternative design, that doesn’t use std::vector.
The primary purpose of using std::vector is that it packs its data altogether in a tight space, which is fantastic for performance. However, you lose all the benefits of that when you store pointers in a vector, because even though the pointers are all nicely packed together, your data can be all the hell over the place. A vector of pointers to stuff has more-or-less no real benefit over any other container type… and in fact is usually worse than other containers because you have to do this two-stage allocation dance: first allocate the actual stuff, then allocate the pointer to the stuff in the vector.
Instead of using a vector, how about using std::forward_list. And let’s throw away ObjectInfo, and just store the Ts directly in the forward list. So how do we know which ones are in use and which aren’t? Simple: two forward lists; one for in-use objects, one for not-in-use objects.
And of course, let’s use standard allocators rather than reinventing the wheel.
So:
template &lt;typename T, typename Allocator = std::allocator&lt;T&gt;&gt; // or std::pmr::allocator&lt;T&gt;, if you prefer
class ObjectPool
{
    std::forward_list&lt;T&gt; _objects;
    std::forward_list&lt;T&gt; _available_list;

public:

    ObjectPool() = default;

    // this constructor allows you to provide a stateful allocator
    explicit ObjectPool(Allocator const&amp; alloc) :
        _objects(alloc),
        _available_list(alloc)
    {}

    // moveable
    ObjectPool(ObjectPool&amp;&amp;) noexcept = default;
    auto operator=(ObjectPool&amp;&amp;) noexcept -&gt; ObjectPool&amp; = default;

    // non-copyable
    ObjectPool(ObjectPool const&amp;) = delete;
    auto operator=(ObjectPool const&amp;) -&gt; ObjectPool&amp; = delete;

    // need to declare this for sanity, because the copy ops are deleted
    ~ObjectPool() = default;

    // ... [snip] ...
};

Now how would we implement getObject()? Well, let’s take it one step at a time. First, how would we implement newObject()? That’s easy:
template &lt;typename... Args&gt;
auto newObject(Args&amp;&amp;... args) -&gt; T*
{
    if (/* check whether there is capacity left - depends on how you want to handle capacity */)
        throw std::runtime_error{&quot;pool is full&quot;};

    std::cout &lt;&lt; &quot;Creating a new object\n&quot;;
    return &amp;(_objects.emplace_front(std::forward&lt;Args&gt;(args)...));
}

What about getAvailableObject()? The first part—checking whether anything is available—is not hard:
auto getAvailableObject() noexcept -&gt; T*
{
    if (_available_list.empty())
        return nullptr;

    // ???
}

If the available list isn’t empty, we want to take an object out of it, and put it into the active objects list. We’ll put it at the very front of the active objects list, because it’s easiest to access it there:
auto getAvailableObject() noexcept -&gt; T*
{
    if (_available_list.empty())
        return nullptr;

    // take the first object in available list (_available_list.begin()), pull
    // it out of the available list, and splice it into the active list before
    // the first object (_objects.before_begin())
    _objects.splice_after(_objects.before_begin(), _available_list, _available_list.begin()).

    return &amp;(_objects.front());
}

And that makes getObject() the same as before:
auto getObject() -&gt; T*
{
    if (auto p = getAvailableObject(); p)
        return p;

    return newObject();
}

What about releaseObject()? Also simple. All we have to do is find the object in the list, then splice it out of _objects and into _available_list:
auto releaseObject(T const* p) -&gt; void
{
    auto it = std::find_if(_objects.begin(), _objects.end(), [p](auto&amp;&amp; o) { return &amp;o == p; });
    if (it != _objects.end())
    {
        _available_list.splice_after(_available_list.before_begin(), _objects, it);
    }
    else
    {
        // tried to release an object that wasn't active in the pool
        //
        // report an error?
    }
}

As for destroy(), it’s completely unnecessary. If you really want to check for any in-use objects when destroying or clearing the pool, you just need to see if _objects is not empty.
This isn’t the only alternative design, and may not even be the best. (The best, if you want a fixed-size object pool, might be to allocate a raw buffer and construct objects in-place as needed, with a bitset to keep track of which positions are available. If you want a growable pool, then maybe the same but with multiple buffers, added as needed… but a growable pool generally makes little sense.) But this design is simple, efficient, and easy to extend. For example, you could pre-allocate a bunch of objects (just create them in _available_objects). You could compact the pool (just clear _available_objects). And if you wanted locality, you could use a std::pmr::monotonic_buffer_resource.
This satisfies your 4 pool requirements:

“Create a pool”: auto pool = ObjectPool&lt;T&gt;{};
“Allocate an object from this pool”: auto* p_obj = pool.getObject();
“Deallocate an object from this pool”: pool.releaseObject(p_obj);
“Destroy this pool”: /* nothing */ (automatic)

Your main() becomes:
int main() {
    auto pool = ObjectPool&lt;int&gt;{};

    auto int1 = pool.getObject();
    auto int2 = pool.getObject();
    auto int3 = pool.getObject();

    pool.releaseObject(int1);

    auto int4 = pool.getObject();

    // pool will now contain 3 objects: int2, int3, and int4
    //
    // int1 will have been released, and replaced with int4

    // the rest is no longer necessary
    // integer::destroy();
    // return 0;
}

As for the question… I don’t understand the confusion. You could already pool all the types you listed even with your original code.