最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c++ - Custom shared_ptr when reaching count 1 - Stack Overflow

programmeradmin2浏览0评论

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
  • Why? What problem does that solve? This sounds a bit like a XY problem. You have a problem and you think that using a used_buffer and a free_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 the free_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
  • 1 Instead of the default deleter for the shared pointer, you could provide your own recycler to put the object back onto your available buffer of objects. (For tracking whether the object is still unused or "dirty", you'd need to figure something else out for that. Such as the object itself keeping track when it becomes dirty.) – Eljay Commented Mar 30 at 15:49
  • 1 @ThomasWeller the problem is obvious: I do not want to allocate the objects each time because allocating objects from heap is quite costly operation. That is why I want to have them pre-allocated. And at the same time use it as a normal shared_ptr. Regarding corner cases in the code (like empty free_buffer): the code was simplified to focus on the main issue. – majvan Commented Mar 30 at 16:01
  • @Eljay you suggest to implement my own "deleter" which would put the object into a buffer. Would that deleter be safe, won't be standard heap free called by standard library? – majvan Commented Mar 30 at 16:06
  • @majvan if you use a custom deleter that buffers objects, there wont be a heap free performed by the standard library. The standard deleter does that free, and you would be replacing the standard deleter – Remy Lebeau Commented Mar 30 at 17:49
Add a comment  | 

3 Answers 3

Reset to default 1

I 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

  1. thread-safety, shared_ptr can be destroyed in any thread (make ObjectFactory thread-safe)
  2. lifetimes, shared_ptr can outlive ObjectFactory, it cannot store this (make the lambda store a weak_ptr to the ObjectFactory instead of this, see enable_shared_from_this)
  3. 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 to A, 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
.

发布评论

评论列表(0)

  1. 暂无评论