C++ - passing references to std::shared_ptr or boost::shared_ptr

前端 未结 17 1558
日久生厌
日久生厌 2020-11-28 01:20

If I have a function that needs to work with a shared_ptr, wouldn\'t it be more efficient to pass it a reference to it (so to avoid copying the shared_ptr

相关标签:
17条回答
  • 2020-11-28 01:49

    The point of a distinct shared_ptr instance is to guarantee (as far as possible) that as long as this shared_ptr is in scope, the object it points to will still exist, because its reference count will be at least 1.

    Class::only_work_with_sp(boost::shared_ptr<foo> sp)
    {
        // sp points to an object that cannot be destroyed during this function
    }
    

    So by using a reference to a shared_ptr, you disable that guarantee. So in your second case:

    Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
    {    
        ...  
        sp->do_something();  
        ...  
    }
    

    How do you know that sp->do_something() will not blow up due to a null pointer?

    It all depends what is in those '...' sections of the code. What if you call something during the first '...' that has the side-effect (somewhere in another part of the code) of clearing a shared_ptr to that same object? And what if it happens to be the only remaining distinct shared_ptr to that object? Bye bye object, just where you're about to try and use it.

    So there are two ways to answer that question:

    1. Examine the source of your entire program very carefully until you are sure the object won't die during the function body.

    2. Change the parameter back to be a distinct object instead of a reference.

    General bit of advice that applies here: don't bother making risky changes to your code for the sake of performance until you've timed your product in a realistic situation in a profiler and conclusively measured that the change you want to make will make a significant difference to performance.

    Update for commenter JQ

    Here's a contrived example. It's deliberately simple, so the mistake will be obvious. In real examples, the mistake is not so obvious because it is hidden in layers of real detail.

    We have a function that will send a message somewhere. It may be a large message so rather than using a std::string that likely gets copied as it is passed around to multiple places, we use a shared_ptr to a string:

    void send_message(std::shared_ptr<std::string> msg)
    {
        std::cout << (*msg.get()) << std::endl;
    }
    

    (We just "send" it to the console for this example).

    Now we want to add a facility to remember the previous message. We want the following behaviour: a variable must exist that contains the most recently sent message, but while a message is currently being sent then there must be no previous message (the variable should be reset before sending). So we declare the new variable:

    std::shared_ptr<std::string> previous_message;
    

    Then we amend our function according to the rules we specified:

    void send_message(std::shared_ptr<std::string> msg)
    {
        previous_message = 0;
        std::cout << *msg << std::endl;
        previous_message = msg;
    }
    

    So, before we start sending we discard the current previous message, and then after the send is complete we can store the new previous message. All good. Here's some test code:

    send_message(std::shared_ptr<std::string>(new std::string("Hi")));
    send_message(previous_message);
    

    And as expected, this prints Hi! twice.

    Now along comes Mr Maintainer, who looks at the code and thinks: Hey, that parameter to send_message is a shared_ptr:

    void send_message(std::shared_ptr<std::string> msg)
    

    Obviously that can be changed to:

    void send_message(const std::shared_ptr<std::string> &msg)
    

    Think of the performance enhancement this will bring! (Never mind that we're about to send a typically large message over some channel, so the performance enhancement will be so small as to be unmeasureable).

    But the real problem is that now the test code will exhibit undefined behaviour (in Visual C++ 2010 debug builds, it crashes).

    Mr Maintainer is surprised by this, but adds a defensive check to send_message in an attempt to stop the problem happening:

    void send_message(const std::shared_ptr<std::string> &msg)
    {
        if (msg == 0)
            return;
    

    But of course it still goes ahead and crashes, because msg is never null when send_message is called.

    As I say, with all the code so close together in a trivial example, it's easy to find the mistake. But in real programs, with more complex relationships between mutable objects that hold pointers to each other, it is easy to make the mistake, and hard to construct the necessary test cases to detect the mistake.

    The easy solution, where you want a function to be able to rely on a shared_ptr continuing to be non-null throughout, is for the function to allocate its own true shared_ptr, rather than relying on a reference to an existing shared_ptr.

    The downside is that copied a shared_ptr is not free: even "lock-free" implementations have to use an interlocked operation to honour threading guarantees. So there may be situations where a program can be significantly sped up by changing a shared_ptr into a shared_ptr &. But it this is not a change that can be safely made to all programs. It changes the logical meaning of the program.

    Note that a similar bug would occur if we used std::string throughout instead of std::shared_ptr<std::string>, and instead of:

    previous_message = 0;
    

    to clear the message, we said:

    previous_message.clear();
    

    Then the symptom would be the accidental sending of an empty message, instead of undefined behaviour. The cost of an extra copy of a very large string may be a lot more significant than the cost of copying a shared_ptr, so the trade-off may be different.

    0 讨论(0)
  • 2020-11-28 01:51

    It is sensible to pass shared_ptrs by const&. It will not likely cause trouble (except in the unlikely case that the referenced shared_ptr is deleted during the function call, as detailed by Earwicker) and it will likely be faster if you pass a lot of these around. Remember; the default boost::shared_ptr is thread safe, so copying it includes a thread safe increment.

    Try to use const& rather than just &, because temporary objects may not be passed by non-const reference. (Even though a language extension in MSVC allows you to do it anyway)

    0 讨论(0)
  • 2020-11-28 01:54

    In the second case, doing this is simpler:

    Class::only_work_with_sp(foo &sp)
    {    
        ...  
        sp.do_something();  
        ...  
    }
    

    You can call it as

    only_work_with_sp(*sp);
    
    0 讨论(0)
  • 2020-11-28 01:56

    Every code piece must carry some sense. If you pass a shared pointer by value everywhere in the application, this means "I am unsure about what's going on elsewhere, hence I favour raw safety". This is not what I call a good confidence sign to other programmers who could consult the code.

    Anyway, even if a function gets a const reference and you are "unsure", you can still create a copy of the shared pointer at the head of the function, to add a strong reference to the pointer. This could also be seen as a hint about the design ("the pointer could be modified elsewhere").

    So yes, IMO, the default should be "pass by const reference".

    0 讨论(0)
  • 2020-11-28 01:57

    Sandy wrote: "It seems that all the pros and cons here can actually be generalised to ANY type passed by reference not just shared_ptr."

    True to some extent, but the point of using shared_ptr is to eliminate concerns regarding object lifetimes and to let the compiler handle that for you. If you're going to pass a shared pointer by reference and allow clients of your reference-counted-object call non-const methods that might free the object data, then using a shared pointer is almost pointless.

    I wrote "almost" in that previous sentence because performance can be a concern, and it 'might' be justified in rare cases, but I would also avoid this scenario myself and look for all possible other optimization solutions myself, such as to seriously look at adding another level of indirection, lazy evaluation, etc..

    Code that exists past it's author, or even post it's author's memory, that requires implicit assumptions about behavior, in particular behavior about object lifetimes, requires clear, concise, readable documentation, and then many clients won't read it anyway! Simplicity almost always trumps efficiency, and there are almost always other ways to be efficient. If you really need to pass values by reference to avoid deep copying by copy constructors of your reference-counted-objects (and the equals operator), then perhaps you should consider ways to make the deep-copied data be reference counted pointers that can be copied quickly. (Of course, that's just one design scenario that might not apply to your situation).

    0 讨论(0)
  • 2020-11-28 01:58

    It seems that all the pros and cons here can actually be generalised to ANY type passed by reference not just shared_ptr. In my opinion, you should know the semantic of passing by reference, const reference and value and use it correctly. But there is absolutely nothing inherently wrong with passing shared_ptr by reference, unless you think that all references are bad...

    To go back to the example:

    Class::only_work_with_sp( foo &sp ) //Again, no copy here  
    {    
        ...  
        sp.do_something();  
        ...  
    }
    

    How do you know that sp.do_something() will not blow up due to a dangling pointer?

    The truth is that, shared_ptr or not, const or not, this could happen if you have a design flaw, like directly or indirectly sharing the ownership of sp between threads, missusing an object that do delete this, you have a circular ownership or other ownership errors.

    0 讨论(0)
提交回复
热议问题