Reputation: 862
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
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
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:
shared_ptr
, as it avoids needing to manipulate the reference count the way you have to if you copy and destroy the source)A(std::move(callers_ptr))
), it move constructs twice (again, avoiding any refcnt manipulation), but again, no copiesSo the costs are:
shared_ptr
)For comparison, option #1 in each scenario requires:
shared_ptr
)const
reference lifetime extension in this scenario; either way, one copy is worse than two moves, given the atomics involved in shared_ptr
s)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 NULL
ing 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