nick
nick

Reputation: 862

Is it necessary to pass smart pointer object by reference?

Assume I have a class, which contains a smart pointer as its member variable:

class B;

class A {
 public:
  A(const std::shared_ptr<B>& b) : b_(b) {}  // option1: passing by reference
  A(std::shared_ptr<B> b) : b_(b) {}  // option2: passing by value
  std::shared_ptr<B> b_;
};

I have two choices for A's constructor: construct by smart pointer and construct by smart pointer's reference.

What are the advantages and disadvantages of these two methods?

Is copying the smart pointer wasteful?

Upvotes: 6

Views: 851

Answers (2)

Hill
Hill

Reputation: 429

I think this answer from Herb Sutter is clear enough

But the Guidelines are simple — don’t pass by smart pointer unless you want to use/modify the smart pointer itself (like any object). The main thing is that pass by value/*/& are all still good and should still be used primarily. It’s just that we now have a couple of idioms for expressing ownership transfer in function signatures, notably passing a unique_ptr by value means “sink” and passing a shared_ptr by value means “gonna share ownership.” That’s pretty much it.

Upvotes: 0

ShadowRanger
ShadowRanger

Reputation: 155418

The best option is option #3:

A(std::shared_pointer<B> b) : b_(std::move(b)) {}  // option3: passing by value and move

Unlike option1, it won't perform an unnecessary copy when the shared_ptr was constructed from a prvalue passed to A's constructor. Unlike option2, it won't perform an unnecessary copy internally.

The costs are:

  1. When passed a prvalue, it performs a single construction and a single move construction (which is more efficient for shared_ptr, as it avoids needing to manipulate the reference count the way you have to if you copy and destroy the source)
  2. When passed any other r-value (e.g. A(std::move(callers_ptr))), it move constructs twice (again, avoiding any refcnt manipulation), but again, no copies
  3. When passed an l-value, it copies once (into the argument, taking ownership before the object is even constructed), then moves into the member cheaply. This is the case where the caller is maintaining ownership while also giving you separate ownership, so the single copy is unavoidable.

So the costs are:

  • prvalue: One move (plus the necessary actual construction of the original shared_ptr)
  • Other r-value: Two moves
  • l-value: One move (plus necessary copy to acquire ownership)

For comparison, option #1 in each scenario requires:

  • prvalue: One copy (plus the necessary construction of the original shared_ptr)
  • Other r-value: One copy (possibly an additional move as well; not 100% on C++ rules for const reference lifetime extension in this scenario; either way, one copy is worse than two moves, given the atomics involved in shared_ptrs)
  • l-value: Just the necessary copy to acquire ownership (an improvement on option #3, which adds a move, but moves are cheap, so saving copies in the other cases is much more important)

Option #2 in each scenario is exactly the same as option #3, but one move from each scenario instead becomes a copy (so option #2 is objectively worse in every scenario); adding std::move to change option #2 to option #3 is a pure win.

So yes, option #1 might be slightly more efficient if callers always retain their own ownership of the shared_ptr while giving the A its own ownership as well, never transferring their ownership to the new A. But each move you're saving is just a couple non-atomic pointer assignments; either way you copy the pointer(s) from source to target, with move adding the NULLing out of the source, while saving a copy means you avoid incrementing the reference count atomically through the pointer to the non-local control block (likely to be an order of magnitude more expensive than local non-atomic pointer assignment).


Note: There is an option #4, as mentioned by Nathan in the comments that is strictly more performant, using a perfect-forwarding constructor to skip a move operation in each case. The downside is the code gets much more complicated, and if the use case is more complex (not just single trivial shared_ptr member), you have the potential problems involved with the (usually not noexcept) construction/copy operations occurring within the constructor, not on the caller side, so the risk of an exception occurring while the object is only partially constructed increases. As long as all non-move operations occur outside the constructor (meaning they're done for the arguments), the constructor itself can often be noexcept and avoid needing to deal with the possibility of a mid-initialization exception.

Upvotes: 9

Related Questions