I have a following issue in C++. I am creating an object factory, which has a method returning shared_ptr to an object of class. The object factory uses a buffer of pre-allocated objects called "free_buffer". After the method is called, it moves the object pointer to its "used_buffer".
Simplified code:
class ObjectFactory {
public:
std::shared_ptr<A> get_object()
{
auto obj = free_buffer.pop_back();
used_buffer.push_back(obj);
return obj;
}
private:
std::vector<std:shared_ptr<A>> used_buffer;
std::vector<std:shared_ptr<A>> free_buffer;
}
I want to make, that if the object returned by get_object is not used by any other owner than the object factory (which has its reference in used_buffer), i.e. its count decreases to 1, it will be moved to the "free_buffer" automatically without explicit check from the caller.
How to make it?
I have a following issue in C++. I am creating an object factory, which has a method returning shared_ptr to an object of class. The object factory uses a buffer of pre-allocated objects called "free_buffer". After the method is called, it moves the object pointer to its "used_buffer".
Simplified code:
class ObjectFactory {
public:
std::shared_ptr<A> get_object()
{
auto obj = free_buffer.pop_back();
used_buffer.push_back(obj);
return obj;
}
private:
std::vector<std:shared_ptr<A>> used_buffer;
std::vector<std:shared_ptr<A>> free_buffer;
}
I want to make, that if the object returned by get_object is not used by any other owner than the object factory (which has its reference in used_buffer), i.e. its count decreases to 1, it will be moved to the "free_buffer" automatically without explicit check from the caller.
How to make it?
Share Improve this question asked Mar 30 at 15:20 majvanmajvan 596 bronze badges 5 |3 Answers
Reset to default 1I guess a much simpler design is possible. You don't can define yor own smart pointers:
class proxy_ptr{
public:
// implement rule of 3 via copy-swap idiom:
proxy_ptr()
: m_ref_idx{ ACQURE_SOME_VALID_IDX() }
{};
proxy_ptr(proxy_ptr const& ini)
: m_ref_idx{ini.m_ref_idx}
{ ++s_buffer[m_ref_idx].m_use_count; };
auto& operator= (proxy_ptr oth) {
swap(oth};
return *this;
};// =
~proxy_ptr()
{ --s_buffer[m_ref_idx].m_use_count; };
void swap(proxy_ptr& oth){
m_ref_idx = std::exchange(oth.m_ref_idx, m_ref_idx);
}; // swap
//smart pointer interface:
auto* operator->(this auto& self)
{ return &**this; };
auto& operator*(this auto& self)
{ return std::forward_like<decltype(self)>
(s_buffer[m_ref_idx].m_data); };
bool operator not() const
{ return not bool{*this}; };
explicit operator bool() const
{ return m_ref_idx < s_buffer.size(); };
class data_t; //actual data;
private:
static constexpr auto null_val =
std::numeric_limits<std::size_t>::max();
std::size_t m_ref_idx = null_val;
struct storage_t{
std::size_t m_use_count;
data_t m_data;
};//storage
// contiguous buffer:
static std::vector<storage_t> s_buffer;
};// proxy_ptr
There are lots of tweaks to this design. For multi-threading you can use atomic types as m_ref_idx
. You can also control const propagation property of proxy_ptr
via implementation of operator*
(this snippet uses propagation). You can also drop the smart pointer interface, and use data_t
as an aggregate, while providing all the abstraction and interface functions for proxy_ptr
. The static preallocated s_buffer
member however, needs explicit definition and initialization in a TU.
This approach tries to avoid explicit memory management, but is also an example of how C++ RAII can be applied to none-pointer resource descriptors.
you can use shared_ptr
custom deleter and have it put the item in the free list instead of deleting it, shared_ptr
doesn't need to actually delete anything, have the shared_ptr
only keep track of external uses. and internally you can store the object any way you want (unordered_map
or std::vector<unique_ptr>
)
struct A {};
class ObjectFactory
{
public:
std::shared_ptr<A> get_object()
{
A* free_object = nullptr;
size_t free_slot = -1;
if (m_free_list.size() == 0)
{
// allocate a new object
m_all_objects.push_back(std::make_unique<A>());
free_object = m_all_objects.back().get();
free_slot = m_all_objects.size() - 1;
}
else
{
// get next free slot index
free_slot = m_free_list.top();
m_free_list.pop();
free_object = m_all_objects[free_slot].get();
}
// create pointer with custom deleter
std::shared_ptr<A> ptr{ m_all_objects[free_slot].get(),
[free_slot, this](A*) { // custom deleter
this->m_free_list.push(free_slot);
} };
return ptr;
}
private:
std::vector<std::unique_ptr<A>> m_all_objects;
std::stack<size_t> m_free_list;
};
multiple porblems you'll have to solve
- thread-safety,
shared_ptr
can be destroyed in any thread (makeObjectFactory
thread-safe) - lifetimes,
shared_ptr
can outliveObjectFactory
, it cannot storethis
(make the lambda store aweak_ptr
to theObjectFactory
instead ofthis
, seeenable_shared_from_this
) - you are forced to heap allocate a control block each time. (you can make a custom allocator for the control blocks, essentially make a struct with a destructor that does this instead, and allocate this struct from an allocator with
allocate_shared
then use the aliasing constructor to create another pointer toA
, but IMHO just allocate A from the custom allocator to begin with)
a little safer version (but with possible memory leaks) is to maintain only the pointer that can be reused.
struct A {};
class ObjectFactory: public std::enable_shared_from_this<ObjectFactory>
{
public:
std::shared_ptr<A> get_object()
{
std::unique_ptr<A> free_object = nullptr;
if (m_free_list.size() == 0)
{
// allocate a new object
free_object = std::make_unique<A>();
}
else
{
// get next free object
free_object = std::move(m_free_list.top());
m_free_list.pop();
}
// create pointer with custom deleter
std::shared_ptr<A> ptr{ free_object.get(),
[factory_weak = this->weak_from_this()](A* ptr)
{ // custom deleter
if (auto factory = factory_weak.lock())
{
factory->m_free_list.push(std::unique_ptr<A>{ ptr });
}
else
{
delete ptr;
}
} };
free_object.release();
return ptr;
}
private:
std::stack<std::unique_ptr<A>> m_free_list;
};
this avoids the problem of lifetime and order of destruction, but it can easily cause memory leaks.
in a resource manager, you typically will want to obtain multiple shared_ptr
to the same object from the factory by name, this can be done using an unordered_map
that keeps a weak_ptr
to the object.
struct holder
{
A object;
std::weak_ptr<A> weak_ref;
};
std::unordered_map<std::string, holder> elements;
and you'll have to populate this weak_ptr
whenever you create a shared_ptr
out of the object, and when you request it by name you just lock
this weak_ptr, so that all existing shared_ptr
will share the same control block.
If the used_buffer is not mandatory, I would suggest:
struct ObjectFactory {
ObjectFactory() {
// put some objects in there
for (auto i = 0; i < 10; i++)
free_buffer.emplace_back(std::make_unique<A>());
}
std::shared_ptr<A> get_object()
{
if (free_buffer.empty() return {};
auto obj = std::move(free_buffer.back());
free_buffer.pop_back();
auto raw = obj.release();
auto o = std::shared_ptr<A>(raw, [this] (auto *ptr) {
free_buffer.emplace_back(ptr);
});
return o;
}
std::vector<std::unique_ptr<A>> free_buffer;
};
Probably need some more work but you didn't give much context, so should be a good place to start.
EDIT:
If object can outlive the factory, you may try:
struct ObjectFactory {
ObjectFactory() {
for (auto i = 0; i < 10; i++)
free_buffer->emplace_back(std::make_unique<A>());
}
std::shared_ptr<A> get_object()
{
auto obj = std::move(free_buffer->back());
free_buffer->pop_back();
auto raw = obj.release();
auto o = std::shared_ptr<A>(raw, [wfb = std::weak_ptr{free_buffer}] (auto *ptr) {
if (auto free_buffer = wfb.lock())
free_buffer->emplace_back(ptr);
else
delete ptr;
});
return o;
}
std::shared_ptr<std::vector<std::unique_ptr<A>>> free_buffer = std::make_shared<std::vector<std::unique_ptr<A>>>();
};
main difference is that the free_buffer is now a shared ptr and we keep a weak ptr in the deleter to eventually delete manually objects if the factory was deleted
.
used_buffer
and afree_buffer
solves that problem. Tell us more about the problem instead of the solution that does not work. Also: which C++ version are you using? What shall happen if there are no more objects in thefree_buffer
? pop_back() invokes UB in that case. From a guess, you are using this in a multi-threaded environment where you want to avoid data races, so that each thread works on an individual object. Also: How many pre-allocated objects do you have? – Thomas Weller Commented Mar 30 at 15:39