Search code examples
c++optimizationshared-ptr

Passing shared_ptr through 2 layers of methods?


What is the most efficient way to give a shared_ptr<void> to an object that passes it to another private method before being stored in it's final destination?

It's difficult to describe... maybe asking with code will illustrate the question better:

class Example{
public:
    void give(std::shared_ptr<void> data) {    // [A] which signature is best here?
    void give(std::shared_ptr<void> & data) {  // [B] 
        // the caller keeps it's shared_ptr
        m_give(data);             // [A] which of these is preferable?
        m_give(std::move(data));  // [B] 
    }

private:
    void m_give(std::shared_ptr<void> data) {    // [A] which signature is best here
    void m_give(std::shared_ptr<void> & data) {  // [B]
    void m_give(std::shared_ptr<void> && data) { // [C] 
        // only `give()` will ever call this
        m_data = data;             // [A] which of these is preferable?
        m_data = std::move(data);  // [B]
    }

    // members
    std::shared_ptr<void> m_data;
};

How would you set up a mechanism like this?


Solution

  • An objective standard is needed for defining a "most efficient way".

    You clarified that you consider just one reference counting increment. I would suggest a slightly non-intuitive solution which does exactly this:

    #include <memory>
    
    class obj {};
    
    class Example {
    
    public:
        void give(std::shared_ptr<obj> data)
        {
            m_give(data);
        }
    
    
    private:
    
        void m_give(std::shared_ptr<obj> & data)
        {
            m_data=std::move(data);
        }
    
        std::shared_ptr<obj> m_data;
    };
    
    int main()
    {
        auto ptr=std::make_shared<obj>();
    
        Example e;
    
        e.give(ptr);
        return 0;
    }
    

    If you use your debugger to run this step by step you should see that e.give(ptr); increments the reference count just once, to copy-construct give()'s parameter. The shared_ptr gets moved into its final resting place, without any further reference counting. The End.

    This is not the only way to achieve that, but this approach has the benefit of giving you the option of optimizing this further. If the caller wishes to give up the ownership of the shared_ptr without any reference counting taking place:

        e.give(std::move(ptr));
    

    The caller effectively gives up its reference. Now, stepping through this odyssey in your debugger will show that two moves take place, one to move-construct give()'s parameter, and the same 2nd move. No change to the reference count.

    With some further work it might be possible to have your cake an eat it too: either have a single reference count increment to preserve the caller's ownership, and a single move operation when not. But the resulting usage syntax might be a little bit inconvenient. If the main goal is to optimize for the case where the caller preserve its ownership, as you implied, this approach results in, pretty much, a no-brainer but you will still reserve the option to decide on a case by case whether each caller can give up its ownership, and if so squeeze a little bit more juice out of this rock.