Search code examples
c++c++11referenceshared-ptr

How do I iterate through a sequence of shared_ptr objects?


This is more a styling than performance question. I have just converted (most of) my pointers to shared_ptr objects, and have reluctantly come to accept weak_ptrs as alternatives to raw pointers. My question is, what is the preferred method of iterating through a sequence (let's say a vector) of shared pointer objects? Here is what I've been doing:

std::vector<std::shared_ptr<A>> my_sequence;
// Do something to fill my_sequence;

for (std::shared_ptr<A> const& ptr : my_sequence)
{
  ptr->AMethod();
}

This goes against the *don't use shared_ptr references* rule though, so what's a good alternative, and why?

Questions I would be asking are; Is the technique robust, ie. For AMethod() super tiny, and my_sequence super large, will this method start to impede performance unecessarily due to shared_ptr copies? Is it readable? Is it simple?


Solution

  • First, a disclaimer:

    shared_ptr isn't a panacea. It should be used when ownership is actually shared. Non-sharing ownership is expressed by unique_ptr and non-ownership is expressed by a (raw) reference. weak_ptr is for a shared_ptr that must be observed but not owned… but isn't a good defensive practice against stale pointers in general. Defaulting to shared_ptr goes directly to the lowest common denominator; it's poor programming practice.


    The choice here is between shared_ptr< T >, shared_ptr< T const >, shared_ptr< T > const &, and T const &, and T &. Let's assume you're not changing anything, but the sequence could change its objects, so const is desirable. That narrows it to shared_ptr< T const >, shared_ptr< T > const &, and T const &.

    Let's reformulate the question as,

    Is shared_ptr< T > const & ever the right choice?

    In this light, evaluate the alternatives.

    • shared_ptr< T const > (pass shared pointer by value)

      • +: straightforward value semantics; can pass shared_ptr< T >
      • +: can be copied or moved to expand the ownership pool
      • +: propagates const correctness to the called function
      • +: access to object is fast: a direct pointer argument is included
      • –: passing is expensive: copies two machine words and atomic-touches a ref count
      • –: called function concerns itself with ownership semantics
    • shared_ptr< T > const & (pass shared pointer by reference)

      • +: passing is as cheap as a direct object reference
      • +: can be copied to expand the ownership pool
      • –: loses const correctness to the called function

        • If you try shared_ptr< T const > const & instead, you will pass a temporary copy and end up with the disadvantages of this alternative and pass-by-value.
      • –: access to object goes through an extra indirection

      • –: called function concerns itself with ownership semantics
    • T const & (direct reference)

      • +: preserves const correctness
      • +: caller unconcerned with ownership semantics
      • +: pass fast, just one machine word
      • +: access fast, direct pointer argument to function (if even that)
      • –: cannot confer ownership

    Weighing it on balance, if ownership isn't being extended (such as by returning an object that retains the argument), you really should pass a simple reference. In such a case, the function should seldom care how its argument is owned. Having it require a shared_ptr violates separation of concerns in the worst way. You can't have a local object or a member subobject and still use the function. And worst of all, everything is more complicated.

    If ownership is potentially extended to someone else, which would be the justification for using shared_ptr in the first place, then you should always pass by shared_ptr< [const] T > value. Yes, it does copy the shared_ptr upon call. But the expensive part of copying a shared_ptr is updating the refcount, not pushing the pointers on the stack. And if you're conferring ownership, you want to update the refcount anyway. Take the passed value and move it, which does not touch the refcount. Having done that, you're left with no advantages and a lot of disadvantages to pass-by-reference.