33

I've been reading quite a number of discussions about performance issues when smart pointers are involved in an application. One of the frequent recommendations is to pass a smart pointer as const& instead of a copy, like this:

void doSomething(std::shared_ptr<T> o) {}

versus

void doSomething(const std::shared_ptr<T> &o) {}

However, doesn't the second variant actually defeat the purpose of a shared pointer? We are actually sharing the shared pointer here, so if for some reasons the pointer is released in the calling code (think of reentrancy or side effects) that const pointer becomes invalid. A situation the shared pointer actually should prevent. I understand that const& saves some time as there is no copying involved and no locking to manage the ref count. But the price is making the code less safe, right?

ks1322
  • 33,961
  • 14
  • 109
  • 164
Mike Lischke
  • 48,925
  • 16
  • 119
  • 181
  • 12
    You should only use the smart pointer to manage the lifetime of the object. When calling down to functions pass the *raw pointer* or a *reference* to the object the *smart pointer* is managing. See: https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rr-smartptrparam – Galik Jun 03 '16 at 09:21
  • 8
    It depends, do you actually need to do operations on the `std::shared_ptr` or just the pointee? If it's the latter, just take a `const T&`; `doSomething` shouldn't need to care about how the object is stored. – TartanLlama Jun 03 '16 at 09:21
  • 1
    I agree with all the above, if it's at all possible, use only a const T&. – Julien Lopez Jun 03 '16 at 09:23
  • @Galik What if the function needs to extend the life of the object? – David Schwartz Jun 03 '16 at 09:26
  • 1
    @DavidSchwartz that falls squarely into the "manage the lifetime of the object" category, doesn't it ? – Quentin Jun 03 '16 at 09:30
  • @Quentin Right. So you can't say "When calling down to functions, pass the raw pointer or a reference to the object the smart pointer is managing" unless you qualify it with "unless those functions may need to extend the lifetime of the object". Most likely, the reason you have a `shared_ptr` at all is because you have a use case where you expect to need to extend the lifetime. – David Schwartz Jun 03 '16 at 09:33
  • @DavidSchwartz I could have chosen my words more carefully, but the supplied link should clarify things – Galik Jun 03 '16 at 09:35
  • @DavidSchwartz if a function `f` calls a function that has an effect on the parameter's lifetime, then transitively `f` itself has an effect on the parameter's lifetime. – Quentin Jun 03 '16 at 09:37
  • @Quentin I agree. Generally, the reason you are passing things around by `shared_ptr` is because they're the kind of things that often need their lifetimes extended. (Though perhaps it's unwise for me to assume that the code is sane.) – David Schwartz Jun 03 '16 at 09:41

6 Answers6

24

The advantage of passing the shared_ptr by const& is that the reference count doesn't have to be increased and then decreased. Because these operations have to be thread-safe, they can be expensive.

You are quite right that there is a risk that you can have a chain of passes by reference that later invalidates the head of the chain. This happened to me once in a real-world project with real-world consequences. One function found a shared_ptr in a container and passed a reference to it down a call stack. A function deep in the call stack removed the object from the container, causing all the references to suddenly refer to an object that no longer existed.

So when you pass something by reference, the caller must ensure it survives for the life of the function call. Don't use a pass by reference if this is an issue.

(I'm assuming you have a use case where there's some specific reason to pass by shared_ptr rather than by reference. The most common such reason would be that the function called may need to extend the life of the object.)

Update: Some more details on the bug for those interested: This program had objects that were shared and implemented internal thread safety. They were held in containers and it was common for functions to extend their lifetimes.

This particular type of object could live in two containers. One when it was active and one when it was inactive. Some operations worked on active objects, some on inactive objects. The error case occurred when a command was received on an inactive object that made it active while the only shared_ptr to the object was held by the container of inactive objects.

The inactive object was located in its container. A reference to the shared_ptr in the container was passed, by reference, to the command handler. Through a chain of references, this shared_ptr ultimately got to the code that realized this was an inactive object that had to be made active. The object was removed from the inactive container (which destroyed the inactive container's shared_ptr) and added to the active container (which added another reference to the shared_ptr passed to the "add" routine).

At this point, it was possible that the only shared_ptr to the object that existed was the one in the inactive container. Every other function in the call stack just had a reference to it. When the object was removed from the inactive container, the object could be destroyed and all those references were to a shared_ptr that that no longer existed.

It took about a month to untangle this.

David Schwartz
  • 179,497
  • 17
  • 214
  • 278
  • 2
    That's exactly the sitution I fear with a shared shared_ptr. – Mike Lischke Jun 03 '16 at 09:30
  • Could you have first added the object to the active objects before removing it from the inactive objects? – gnasher729 Jun 03 '16 at 16:01
  • @gnasher729 My recollection is that in our particular case that wouldn't have been possible because that would require an intermediate point where the object was an in illegal state and no locks were held. But that was a quirk of our particular situation. Otherwise, add before remove would have solved it. – David Schwartz Jun 03 '16 at 16:16
  • 1
    "the code that calls it doesn't really know it can remove the very object it has a reference to" -- I'd say this as the point of failure. If you have a `shared_ptr` from a container, you know the container may be modified but don't know exactly what modifications are going to be made, then you *cannot* pass around a reference to that `shared_ptr`. And in fact the same is true if we replace `shared_ptr` in the previous sentence with `int`, and the same would be true if someone asked "I've noticed that passing ints by reference is faster than values, should I blindly replace all calls?". No. – Steve Jessop Jun 03 '16 at 16:26
  • 1
    So I think this answer is right to caution not routinely changing all pass-by-value to pass-by-reference just because the object happens to be expensive to copy. The fact that the reason isn't specific to `shared_ptr` doesn't stop it being a critical reason for not doing it :-) You can't just pass around references to things whose lifetime you don't ensure, no matter how expensive. And your container example probably went wrong because someone assumed "ah, there's a `shared_ptr` involved, the lifetime will all be fine because `shared_ptr` is doing its thing", when it wasn't. – Steve Jessop Jun 03 '16 at 16:31
12

First of all, don't pass a shared_ptr down a call chain unless there is a possibility that one of the called functions will store a copy of it. Pass a reference to the referred object, or a raw pointer to that object, or possibly a box, depending on whether it can be optional or not.

But when you do pass a shared_ptr, then preferably pass it by reference to const, because copying a shared_ptr has additional overhead. The copying must update the shared reference count, and this update must be thread safe. Hence there is a little inefficiency that can be (safely) avoided.

Regarding

the price is making the code less safe, right?

No. The price is an extra indirection in naïvely generated machine code, but the compiler manages that. So it's all about just avoiding a minor but totally needless overhead that the compiler can't optimize away for you, unless it's super-smart.

As David Schwarz exemplified in his answer, when you pass by reference to const the aliasing problem, where the function you call in turn changes or calls a function that changes the original object, is possible. And by Murphy's law it will happen at the most inconvenient time, at maximum cost, and with the most convoluted impenetrable code. But this is so regardless of whether the argument is a string or a shared_ptr or whatever. Happily it's a very rare problem. But do keep it in mind, also for passing shared_ptr instances.

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • Your point that this issue with references changing is not a `shared_ptr` specific problem is a very important one! I think, of the answers, it points at the *actual* problem the best. – Cort Ammon Jun 03 '16 at 14:39
7

First of all there is a semantic difference between the two: passing shared pointer by value indicates your function is going to take its part of the underlying object ownership. Passing shared_ptr as const reference does not indicate any intent on top of just passing the underlying object by const reference (or raw pointer) apart from inforcing users of this function to use shared_ptr. So mostly rubbish.

Comparing performance implications of those is irrelevant as long as they are semantically different.

from https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/

Don’t pass a smart pointer as a function parameter unless you want to use or manipulate the smart pointer itself, such as to share or transfer ownership.

and this time I totally agree with Herb :)

And another quote from the same, which answers the question more directly

Guideline: Use a non-const shared_ptr& parameter only to modify the shared_ptr. Use a const shared_ptr& as a parameter only if you’re not sure whether or not you’ll take a copy and share ownership; otherwise use * instead (or if not nullable, a &)

Slava
  • 1,528
  • 1
  • 15
  • 23
  • That's true, but not really relevant to the question. – David Schwartz Jun 03 '16 at 09:42
  • I think that is very relevant, but I added more explicit quote from the same. Note that the more explicit quote for me hints for a smelly design, so I tend to say const shared_ptr<>& is mostly useless as a function parameter. – Slava Jun 03 '16 at 09:48
  • The question was about passing a `shared_ptr` by value versus passing it by const reference. Nothing in your answer addresses that. – David Schwartz Jun 03 '16 at 09:54
5

As pointed out in C++ - shared_ptr: horrible speed, copying a shared_ptr takes time. The construction involves an atomic increment and the destruction an atomic decrement, an atomic update (whether increment or decrement) may prevent a number of compiler optimizations (memory loads/stores cannot migrate across the operation) and at hardware level involves the CPU cache coherency protocol to ensure that the whole cache line is owned (exclusive mode) by the core doing the modification.

So, you are right, std::shared_ptr<T> const& may be used as a performance improvement over just std::shared_ptr<T>.


You are also right that there is a theoretical risk for the pointer/reference to become dangling because of some aliasing.

That being said, the risk is latent in any C++ program already: any single use of a pointer or reference is a risk. I would argue that the few occurrences of std::shared_ptr<T> const& should be a drop in the water compared to the total number of uses of T&, T const&, T*, ...


Lastly, I would like to point that passing a shared_ptr<T> const& is weird. The following cases are common:

  • shared_ptr<T>: I need a copy of the shared_ptr
  • T*/T const&/T&/T const&: I need a (possibly null) handle to T

The next case is much less common:

  • shared_ptr<T>&: I may reseat the shared_ptr

But passing shared_ptr<T> const&? Legitimate uses are very very rare.

Passing shared_ptr<T> const& where all you want is a reference to T is an anti-pattern: you force the user to use shared_ptr when they could be allocating T another way! Most of the times (99,99..%), you should not care how T is allocated.

The only case where you would pass a shared_ptr<T> const& is if you are not sure whether you will need a copy or not, and because you have profiled the program and showed that this atomic increment/decrement was a bottleneck you have decided to defer the creation of the copy to only the cases where it is needed.

This is such an edge case that any use of shared_ptr<T> const& should be viewed with the highest degree of suspicion.

Community
  • 1
  • 1
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Your reference to that other question is excellently answering my implicit question about performance, which wasn't covered so far. – Mike Lischke Jun 03 '16 at 13:35
  • @MikeLischke: Precisely why I included it ;) – Matthieu M. Jun 03 '16 at 13:38
  • There's nothing weird, and null-references don't exist in a valid C++ program. Nice with lthe link to more info though. So, + and -, = no voting. :) – Cheers and hth. - Alf Jun 03 '16 at 13:47
  • @Cheersandhth.-Alf: null reference refers to the pointers (which may be null), they are not "reference" as in the Standard... I can see how that's confusing :x I've changed to "handle" instead, is that better? – Matthieu M. Jun 03 '16 at 14:11
4

If no modification of ownership is involved in your method, there's no benefit for your method to take a shared_ptr by copy or by const reference, it pollutes the API and potentially incur overhead (if passing by copy)

The clean way is to pass the underlying type by const ref or ref depending of your use case

void doSomething(const T& o) {}

auto s = std::make_shared<T>(...);
// ...
doSomething(*s);

The underlying pointer can't be released during the method call

Stephane Molina
  • 121
  • 1
  • 7
  • This doesn't answer the question asked. The question was about passing a `shared_ptr` by value versus passing a `shared_ptr` by const reference. – David Schwartz Jun 03 '16 at 09:54
  • the thing is there's absolutely no use to pass a shared_ptr by const & except if you want to pollute the API, where as shared_ptr by copy might be useful if in the scope of your method you will create some wrapper that will outlive the original shared_ptr – Stephane Molina Jun 03 '16 at 10:02
3

I think its perfectly reasonable to pass by const & if the target function is synchronous and only makes use of the parameter during execution, and has no further need of it upon return. Here it is reasonable to save on the cost of increasing the reference count - as you don't really need the extra safety in these limited circumstances - provided you understand the implications and are sure the code is safe.

This is as opposed to when the function needs to save the parameter (for example in a class member) for later re-reference.

Smeeheey
  • 9,906
  • 23
  • 39
  • "This is as opposed to when the function needs to save the parameter (for example in a class member) for later re-reference." is meaningless. Nothing probits you from copying a `shared_ptr` passed by reference to `const`. – Cheers and hth. - Alf Jun 03 '16 at 10:40
  • 1
    Well its true that nothing prohibits you from copying a `const &`. However, I would argue that it makes things clearer having the function take the parameter by value if that is your intention. This clarity doesn't come at the expense of a slower operation either: as you can always move the `shared_ptr` by-value parameter into its final destination within your function should you need to, so you wouldn't need an extra copy – Smeeheey Jun 03 '16 at 10:45
  • Yes, when the function doesn't in turn pass on the `shared_ptr`. In fact the pass-by-value-for-moving is what Herb recommends, so if that's what you meant then I was wrong that it was meaningless. But when you have a call chain, even just two levels, the costs go the other way (copy+copy+move versus ref+ref+copy+move), favoring pass by reference to `const` if one has to choose a single convention. – Cheers and hth. - Alf Jun 03 '16 at 11:11
  • Again even in a chain if the code is organised in a certain way (i.e. such that the chaining call is the last use of the `shared_ptr` at any given level), then you can just `std::move` your shared pointer down the chain, which avoids the extra copies you mentioned. Having said that, I'm sure there are still situations where your approach is more appropriate. I just don't agree with the initial sentiment that my original comment is "meaningless" – Smeeheey Jun 03 '16 at 11:16
  • Usually one can't just `move` objects down a call chain, because usually a call is not the last place in a block where the object is used. So that doesn't work as a general convention. It's a very special case. – Cheers and hth. - Alf Jun 03 '16 at 11:31