lo tolmencre
lo tolmencre

Reputation: 3954

returning rvalue? Or : Why does copy constructor get called on `return <expression>`?

I am suspecting I vaguely know the reason for what I am observing, but I would like confirmation or correction and some explanation to it.

I have the following code:

template <class T>
class C
{

public:

    C () = default;

    C(const C& rhs) : mem(rhs.mem)
    {
        std::cerr << "copy" << "\n";
    }

    // does not call copy constructor twice
    // friend C operator<<(C& src, unsigned shift)
    // {
    //     std::cerr << 1 << "\n";
    //     C tmp(src);
    //     std::cerr << 2 << "\n";
    //     tmp <<= shift;
    //     std::cerr << 5 << "\n";
    //     return tmp;
    // }

    // does call copy constructor twice
    friend C operator<<(C& src, unsigned shift)
    {
        std::cerr << 1 << "\n";
        C tmp(src);
        std::cerr << 2 << "\n";
        return (tmp <<= shift);
    }

    friend C& operator<<=(C& src, unsigned shift)
    {
        std::cerr << 3 << "\n";
        src.mem <<= shift;
        std::cerr << 4 << "\n";
        return src;
    }

    T mem;
};

int main()
{
    C<int> c1;

    c1 << 3;
}

I have two versions of C<T>::operator<<(C<T>, unsigned)

the difference is the one returns the result of an expression:

return (tmp <<= shift);

the other returns a variable:

return tmp

So far I thought the two functions would be semantically identical and the one with return (tmp <<= shift); simply be better style, like return a + 1 would be better style than int ret = a + 1; return ret. This is apparently not so and probably only holds true for atomic data types. The output of the version with return (tmp <<= shift); looks like this:

1
copy
2
3
4
copy

The output of the other like this:

1
copy
2
3
4
5

Is my assumption right that infact return (tmp <<= shift); does not return tmp after invoking <<= on itm but rather create a new object as a copy of tmp after <<= has been invoked?

Upvotes: 1

Views: 177

Answers (2)

apple apple
apple apple

Reputation: 10604

the operator <<= may not return the reference to the input parameter (maybe it will return reference to another static/global variable), so the compiler can not easily decide to use return value optimization, and need to invoke the copy constructor.

In your question: When you write return (tmp <<= shift) the compiler doesn't know whether or not (tmp <<= shift) will return reference to tmp, but if you write return tmp the compiler knows it and can optimize it out.

Upvotes: 0

M.M
M.M

Reputation: 141638

When returning by value, the return value must be initialized. The return value is an object of type C here.

In the case return (tmp <<= shift);, it means that (tmp << shift) is the initializer for a C. Since that is an lvalue of type C, this is a copy-construction.

There is a special rule that for a return statement of the form return identifier; , then it can be move-construction even though identifier is an lvalue. But that rule doesn't extend to other expressions (yet).

The other version of your code does activate the special rule:

tmp <<= shift;
return tmp;

Here tmp may be treated as an rvalue, making it movable (and also this makes it a copy elision context).

In your testing your compiler implemented copy-elision. To test without copy-elision (if your compiler supports letting the user configure that) you'd need to also give your C a move-constructor. A user-provided copy-constructor suppresses implicit generation of a move-constructor.

Upvotes: 1

Related Questions