3

For example

class A
{
public:
    // Option 1
    void setI_1(shared_ptr<int> i) { m_i = i; }

    // Option 2
    void setI_2(shared_ptr<int> i) { m_i = move(i); }

    // Option 3
    void setI_3(shared_ptr<int> const& i) { m_i = i; }

private:
    shared_ptr<int> m_i;        
};

Usually which option is better?

I do benchmark on Visual Studio 2017. Option 2 gives me the best performance in all cases I tested. In some cases, option 3 has similar performance as option 2, but some it is worse. Thanks!

user1899020
  • 13,167
  • 21
  • 79
  • 154
  • Benchmark it. I wouldn't be surprised if this depends on the compiler (some old compilers have quirks with pass-by-value and const reference), however, for modern compilers, I wouldn't be surprised if they're all equal. – Alex Huszagh Mar 27 '18 at 21:51
  • option 3 is better. See https://stackoverflow.com/questions/3310737/should-we-pass-a-shared-ptr-by-reference-or-by-value – M.M Mar 27 '18 at 21:52
  • Option 3 is best. You can still check the produced code if you desire. – DeiDei Mar 27 '18 at 21:52
  • 1
    Somewhat unsurprisingly, using clang, I get the best assembly when using an l-value reference for "Option 2 and 3", however, with a prvalue, option 2 is the best. Obviously, mileage may vary. Use option 3. – Alex Huszagh Mar 27 '18 at 22:07
  • @AlexanderHuszagh (and others suggesting option 3): Why would option 3 be best, given it's equivalent to 2 for l-values and worse for r-values? [The usual rule](https://web.archive.org/web/20140113221447/http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/) is that if you'd need to make a copy inside a function (or constructor) anyway, let the compiler do it for you by accepting by value. In this case, option 2 can (for a prvalue `shared_ptr`) avoid any reference count changes inside the constructor at all, where option 3 needs to manipulate them twice (copy, then delete prvalue). – ShadowRanger Mar 27 '18 at 23:12
  • 1
    @M.M: The answer you link is specific to a case where it's being passed to a function (with no indication that a persistent copy will be made), and (at least according to comments) is avoiding addressing pass-by-value followed by `std::move` (option 2 here) for the "storing a copy" case because it relies on C++11 features that weren't always available at the time it was posted. Given C++11 is ubiquitous at this point, and this question is specifically storing off a copy to a member, not just transient use, I don't think your linked answer applies. – ShadowRanger Mar 28 '18 at 02:01

1 Answers1

0

In your case option 2 is the best case. You may want to move a pointer to the class, so you do not need to increase and decrease the reference-counter again and again, when you do not need it anymore:

{
    shared_ptr<int> ptr = std::make_shared<int>(42);

    A a;
    a.setI_2(std::move(ptr)); // I no longer need ptr, so I can move it
};

The pointer is moved to the parameter list and ptr is null/the parameter is initialized with an r-value-reference referring to ptr, so that ptr gets null. Then it is moved from the parameter list to the member and the parameter is null. You see, there is no (need for a) copy, which implies a change of the reference-counter.

I prefer T const& only for functions which only read from reference and are not used to initialize another variable. If in the example the third option with the reference is used, the pointer needs to be copied, because the body of setI_3 has a copy in the assignment statement (no move).

The first option is obviously being the worst, because the pointer is copied multiple times.

When you exchange the shared_ptr with unique_ptr the compiler forces you to use the fastest way possible -- the moves.

I would add a fourth option:

// Option 4
void setI_4(shared_ptr<int>&& i) { m_i = move(i); }

Note that even of i beeing an r-value-reference, it is not an r-value-reference when used in the code and so would be copied, when there is no move;

The fourth option only differs from the second option, that the passed parameter always needs to be an r-value-reference, and in the second, i is a pointer-variable on its own and in the fourth i is a reference (to something the body of the function can do anything with, but which lives outside the function (the std::move gives permission to do that)). The parameter-variable is constructed from the expression in the parameter-tuple from the caller, the constructor of shared_ptr automatically copies the pointer (which means that the reference-counter is increased) if it is not an r-value-reference.

The calling of setI_2 works as the following:

{
    shared_ptr<int> ptr = std::make_shared<int>(42);
    /// ptr owns 42
    A a;
    {   // a.setI_2(std::move(ptr));
        // initializing the parameter
        shared_ptr<int> i{std::move(ptr)};
        // now i owns 42 and ptr no longer owns it and points to null
        {   // the body of the function
            a.m_i = std::move(i);
            // now a.m_i owns 42 and i no longer owns it and points to null
        };
    };
};
cmdLP
  • 1,658
  • 9
  • 19