Search code examples
c++c++17copy-elision

Guaranteed elision and chained function calls


Let's say I have the following type:

struct X {
    X& operator+=(X const&);
    friend X operator+(X lhs, X const& rhs) {
        lhs += rhs;
        return lhs;
    }
};

And I have the declaration (assume all the named variables are lvalues of type X):

X sum = a + b + c + d;

In C++17, what are the guarantees I have about how many copies and moves this expression will perform? What about non-guaranteed elision?


Solution

  • This will perform 1 copy construction and 3 move constructions.

    1. Make a copy of a to bind to lhs.
    2. Move construct lhs out of the first +.
    3. The return of the first + will bind to the by value lhs parameter of the second + with elision.
    4. The return of the second lhs will incur the second move construction.
    5. The return of the third lhs will incur the third move construction.
    6. The temporary returned from the third + will be constructed at sum.

    For each of the move constructions described above, there is another move construction that is optionally elided. So you are only guaranteed to have 1 copy and 6 moves. But in practice, unless you -fno-elide-constructors, you will have 1 copy and 3 moves.

    If you don't reference a after this expression, you could further optimize with:

    X sum = std::move(a) + b + c + d;
    

    resulting in 0 copies and 4 moves (7 moves with -fno-elide-constructors).

    The above results have been confirmed with an X which has instrumented copy and move constructors.


    Update

    If you're interested in different ways to optimize this, you could start with overload the lhs on X const& and X&&:

    friend X operator+(X&& lhs, X const& rhs) {
        lhs += rhs;
        return std::move(lhs);
    }
    friend X operator+(X const& lhs, X const& rhs) {
        auto temp = lhs;
        temp += rhs;
        return temp;
    }
    

    This gets things down to 1 copy and 2 moves. If you are willing to restrict your clients from ever catching the return of your + by reference, then you can return X&& from one of the overloads like this:

    friend X&& operator+(X&& lhs, X const& rhs) {
        lhs += rhs;
        return std::move(lhs);
    }
    friend X operator+(X const& lhs, X const& rhs) {
        auto temp = lhs;
        temp += rhs;
        return temp;
    }
    

    Getting you down to 1 copy and 1 move. Note that in this latest design, if you client ever does this:

    X&& x = a + b + c;
    

    then x is a dangling reference (which is why std::string does not do this).