So I have been making a object pool class, it is used like so:
class MagicTrick {
public:
MagicTrick(int magic) : _magic(magic)
{}
int predict() {
return _number * _magic;
}
private:
int _magic;
int _number = 2;
};
const std::size_t poolSize = 1;
ObjectPool<MagicTrick> magicTrickPool(poolSize, 5);
const int number = magicTrickPool.schedule([](MagicTrick& magicTrick){
return magicTrick.predict();
});
Which works fine, however when the object used by the thread pool has its copy constructor deleted, e.g. an data member is an std::unique_ptr
the construction of the pool fails.
Internally I use a vector to store the pool:
struct ObjectAndLock {
Object object;
bool free;
static bool isFree(const ObjectAndLock& objectAndLock) {
return objectAndLock.free;
}
};
std::vector<ObjectAndLock> objectAndLocks;
And I construct the full pool class:
template<typename Object>
class ObjectPool {
template<typename ...Args>
ObjectPool(std::size_t poolSize, Args&&... objectArgs)
: objectAndLocks(poolSize, { {std::forward<Args>(objectArgs)...}, true})
{}
This constructs the vector with the 3rd overload listed here https://en.cppreference.com/w/cpp/container/vector/vector
This however copies the element in to the vector.
So I change it to emplace_back
to construct the objects in place in the vector like so:
template<typename Object>
class ObjectPool {
template<typename ...Args>
ObjectPool(std::size_t poolSize, Args&&... objectArgs)
{
if(poolSize == 0){
throw std::runtime_error("poolSize must be greater than 0");
}
objectAndLocks.reserve(poolSize);
for (std::size_t i = 0; i < poolSize; i++)
{
objectAndLocks.emplace_back({Object{std::forward<Args>(objectArgs)...}, true});
}
}
}
This however errors with:
Projects\ObjectPool\public_include\ObjectPool\ObjectPool.hpp(87): error C2660: 'std::vector<object_pool::ObjectPool<MagicTrick>::ObjectAndLock,std::allocator<_Ty>>::emplace_back': function does not take 1 arguments
with
[
_Ty=object_pool::ObjectPool<MagicTrick>::ObjectAndLock
]
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.23.28105\include\vector(651): note: see declaration of 'std::vector<object_pool::ObjectPool<MagicTrick>::ObjectAndLock,std::allocator<_Ty>>::emplace_back'
with
[
_Ty=object_pool::ObjectPool<MagicTrick>::ObjectAndLock
]
However I can use an initializer list to create an object in the constructor like so, which compiles fine.
ObjectAndLock hello = { Object{std::forward<Args>(objectArgs)...}, true };
I have seen this answer, however I was unable to get it to work:
emplace_back not working with std::vector<std::map<int, int>>
I used the tempate for std::initializer_list
as:
std::initializer_list<ObjectAndLock>
perhaps this is wrong?
So my question is how do I get emplace_back
to work correctly? I can use up to c++17
Here is an example class that fails as it is noncopyable:
struct NonCopyable {
std::unique_ptr<int> number = std::make_unique<int>(10);
NonCopyable(const NonCopyable& other) = delete;
NonCopyable& operator=(const NonCopyable& other) = delete;
};
For completeness here is the full class:
#ifndef OBJECTPOOL_H
#define OBJECTPOOL_H
#include <vector>
#include <functional>
#include <map>
#include <mutex>
#include <condition_variable>
#include <type_traits>
#include <algorithm>
#include <stdexcept>
#include <exception>
namespace object_pool {
namespace internal {
template <typename Function>
class DeferToDestruction {
Function _function;
public:
DeferToDestruction(Function function) : _function(function) {}
~DeferToDestruction() { _function(); }
};
}
template<typename Object>
class ObjectPool {
public:
/*!
@brief Create an object pool for
@param poolSize - Size of object pool, this must be atleast 1
@param objectArgs... - Arguments to construct the objects in the pool
Complete Example:
@code
class MagicTrick {
public:
MagicTrick(int magic) : _magic(magic)
{}
int predict() {
return _number * _magic;
}
private:
int _magic;
int _number = 2;
};
std::size_t poolSize = 5;
object_pool::ObjectPool<MagicTrick> magicTrickPool(poolSize, 5);
const int number = magicTrickPool.schedule([](MagicTrick& magicTrick){
return magicTrick.predict();
});
@endcode
Zero Argument Constructor Example:
@code
struct ZeroArgs {
int number = 2;
};
object_pool::ObjectPool<ZeroArgs> zeroArgsPool(1);
@endcode
Multiple Argument Constructor Example:
@code
class MultiArgs {
public:
MultiArgs(std::string name, int age, bool alive) {
_number = name.size() + age + (alive ? 5 : -5);
}
int predict() {
return _number * 2;
}
private:
int _number = 2;
};
object_pool::ObjectPool<MultiArgs> multiArgsPool(1, "bob", 99, true);
@endcode
*/
template<typename ...Args>
ObjectPool(std::size_t poolSize, Args&&... objectArgs)
{
if(poolSize == 0){
throw std::runtime_error("poolSize must be greater than 0");
}
objectAndLocks.reserve(poolSize);
for (std::size_t i = 0; i < poolSize; i++)
{
objectAndLocks.emplace_back({Object{std::forward<Args>(objectArgs)...}, true});
}
}
~ObjectPool(){
std::unique_lock<std::mutex> lock(objectAndLocksMutex);
const auto allobjectAndLocksFree = [this]() {
return std::all_of(std::begin(objectAndLocks), std::end(objectAndLocks), ObjectAndLock::isFree);
};
if(allobjectAndLocksFree()) {
return;
}
conditionVariable.wait(lock, allobjectAndLocksFree);
}
/*!
@brief Schedule access to the pool
@param callback - An callable with the the argument being a reference to the class stored in the object pool.
@return Returns return from the callback function, including void
Simple Example:
@code
const int number = magicTrickPool.schedule([](MagicTrick& magicTrick){
return magicTrick.predict();
});
@endcode
*/
template<typename FunctionWithObjectAsParameter>
auto schedule(FunctionWithObjectAsParameter&& callback)
{
const auto findFreeObject = [this]() {
return std::find_if(std::begin(objectAndLocks), std::end(objectAndLocks), ObjectAndLock::isFree);
};
std::unique_lock<std::mutex> lock(objectAndLocksMutex);
auto freeObject = findFreeObject();
if(freeObject == std::end(objectAndLocks)) {
conditionVariable.wait(lock, [this, &freeObject, &findFreeObject]{
freeObject = findFreeObject();
return freeObject != std::end(objectAndLocks);
});
}
freeObject->free = false;
lock.unlock();
internal::DeferToDestruction freeAndUnlockAndNotify([this, &freeObject] () {
{
std::scoped_lock<std::mutex> lock(objectAndLocksMutex);
freeObject->free = true;
}
conditionVariable.notify_one();
});
return callback(freeObject->object);
}
private:
struct ObjectAndLock {
Object object;
bool free;
static bool isFree(const ObjectAndLock& objectAndLock) {
return objectAndLock.free;
}
};
std::vector<ObjectAndLock> objectAndLocks;
std::mutex objectAndLocksMutex;
std::condition_variable conditionVariable;
};
}
#endif
If you look at the signature of emplace_back
template <class... Args>
reference emplace_back(Args&&... args);
you will find that the types of the parameters of emplace_back
are deduced from the arguments you pass. A braced-init-list can be used to initialize an argument for a parameter of a certain type. But the {…}
does not have a type itself and, thus, cannot be used to deduce the type of the parameter from.
What emplace_back
does is simply std::forward
whatever arguments you pass to it to the constructor of the element type to create an element in-place in the vector. The problem with that would be that your
struct ObjectAndLock {
Object object;
bool free;
static bool isFree(const ObjectAndLock& objectAndLock) {
return objectAndLock.free;
}
};
doesn't even have a constructor that takes arguments (except for the implicit copy and move constructors).
What you would have to do is
objectAndLocks.emplace_back(ObjectAndLock{Object{std::forward<Args>(objectArgs)...}, true});
i.e., initialize a value of the right type for emplace_back
to forward to the implicit move constructor. But that's essentially the same thing as just doing
objectAndLocks.push_back({Object{std::forward<Args>(objectArgs)...}, true});
The braced-init-list works with push_back
because push_back
void push_back(const T& value);
void push_back(T&& value);
does expect a value of the element type rather than a pack of forwarding references and, thus, the {…}
will end up initializing an argument of the appropriate type…
C++20 will introduce direct-initialization via T(…)
for aggregates which will make it possible to simply write
objectAndLocks.emplace_back(Object{std::forward<Args>(objectArgs)...}, true);
here. Until then, I would recommend to just use push_back
in this case…
Your
struct NonCopyable {
std::unique_ptr<int> number = std::make_unique<int>(10);
NonCopyable(const NonCopyable& other) = delete;
NonCopyable& operator=(const NonCopyable& other) = delete;
};
will be not be copyable but also not moveable. You declared a copy constructor which means that there will be no implicitly-declared move constructor [class.copy]/8.1. The story is pretty much the same for the implicitly-declared move assignment operator [class.copy.assign]/4. You simply can't have an immoveable type as element type of a std::vector
. To make NonCopyable
moveable, you'll have to define a move constructor and move assignment operator:
struct NonCopyable {
std::unique_ptr<int> number = std::make_unique<int>(10);
NonCopyable(const NonCopyable&) = delete;
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(const NonCopyable&) = delete;
NonCopyable& operator=(NonCopyable&&) = default;
};