1

This question is different from all of:

which are suggested by stack overflow.


Say you have this simple code.

#include <iostream>
using namespace std;

class Int {

    private:

        int i_;

    public:

        Int(Int &&obj) : i_(obj.i_) { //move constructor
            print_address_change_(&obj);
            cout << i_ << " moved.\n";
        }

        Int(const Int &obj) : i_(obj.i_) { //copy constructor
            print_address_change_(&obj);
            cout << i_ << " copied.\n";
        }

        Int(int i) : i_(i) {
            print_address_();
            cout << i_ << " constructed.\n";
        }

        ~Int() {
            print_address_();
            cout << i_ << " destructed.\n";
        }

        void print_address_() const {
            cout << "(" << this << ") ";
        }

        void print_address_change_(const Int *p) const {
            cout << "(" << p << " -> " << this << ") ";
        }

        const Int operator * (const Int &rhs) const {
            return Int(i_ * rhs.i_);
        }

};

int main() {

    Int i(3);
    Int j(8);

    cout << "---\n";
    Int k = i * j;
    cout << "---\n";

}

The result (by g++ 7.3.0 with default option) is this.

(0x7ffd8e8d11bc) 3 constructed. //i
(0x7ffd8e8d11c0) 8 constructed. //j
---
(0x7ffd8e8d11c4) 24 constructed. //tmp
---
(0x7ffd8e8d11c4) 24 destructed. //k
(0x7ffd8e8d11c0) 8 destructed. //j
(0x7ffd8e8d11bc) 3 destructed. //i

OK. A little strange but you can say copy elision must have occurred. So now with -fno-elide-constructors option, you get the following result.

(0x7ffd8f7693f8) 3 constructed. //i
(0x7ffd8f7693fc) 8 constructed. //j
---
(0x7ffd8f7693c4) 24 constructed. //tmp
(0x7ffd8f7693c4 -> 0x7ffd8f769404) 24 moved. //tmp -> ??? (WHY?)
(0x7ffd8f7693c4) 24 destructed. //tmp
(0x7ffd8f769404 -> 0x7ffd8f769400) 24 copied. //??? -> k (WHY?)
(0x7ffd8f769404) 24 destructed. //??? (WHY?)
---
(0x7ffd8f769400) 24 destructed. //k
(0x7ffd8f7693fc) 8 destructed. //j
(0x7ffd8f7693f8) 3 destructed. //i

This includes three more lines (which marked as "WHY") than I expected. What is ???? Could anyone please tell me what happened there?

ynn
  • 3,386
  • 2
  • 19
  • 42
  • 3
    Move construction to move the returned value, and a copy-construction of `k`. Stepping through the code with the debugger should confirm this. Remember, copy elision has been turned-off. – Sam Varshavchik Mar 17 '19 at 21:04
  • 2
    Returning a `const Int` from `operator*` seems strange. It's the reason you get a copy and not a second move. – super Mar 17 '19 at 21:05
  • 2
    When you return something without copy elision, it has to be copied/moved to a temporary object. You create a temporary with `Int(i_ * rhs.i_)`, which is then moved into another temporary when you return it. – HolyBlackCat Mar 17 '19 at 21:07
  • @HolyBlackCat Thank you. I understand now. – ynn Mar 17 '19 at 21:15
  • @super Is that so? This implementation is (edited) excerpt from *Effective C++ 3rd Edition*. You can see the related page from [GoogleBooks](https://books.google.co.jp/books?id=Qx5oyB49poYC&printsec=frontcover&hl=ja#v=onepage&q=something%20essentially%20equivalent&f=false). I wonder if there is a better implementation. – ynn Mar 17 '19 at 21:18
  • @ynn You can return `Int` instead of `const Int`. I don't see any reason you would want to return a `const` variable in this case. Do you? – super Mar 17 '19 at 21:23
  • All you want to know about this, you will find by reading about RVO (Return Value Optimization). Your `-fno-elide-constructors` option disabled RVO. – Ben Voigt Mar 17 '19 at 21:27
  • @super Yes. I deliberately return `const Int` instead of `Int`. If you return `Int`, expression like `if (a * b = c)` (this is typo of `if (a * b == c)`) is **well-formed**. This is not good in terms of class design, and returning `const Int` is so recommended in the book. (Also, returning `Int` doesn't change the result in this case as far as I tested now.) – ynn Mar 17 '19 at 21:30
  • @ ynn No. Second copy was changed to move, as @super said. – ynn Mar 17 '19 at 22:01

2 Answers2

3

operator* constructs a temporary object with Int(i_ * rhs.i_). It returns that object, which constructs a second temporary object outside the function. That temporary object gets copied into k.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165
1

When you write:

Int k = i * j;

You are actually doing something like:

Int tmp1(i.operator*(j));
Int k(tmp1);

That is, initialization with = is actually used by creating a temporary and doing a copy-construction. Compare with Int k(i * j) or Int k{i *j} that does direct initialization.

Not that it really matters in this case, because even if you write Int k(i*j) a temporary is still needed to hold the return value of operator* before calling the copy constructor of k.

The operator* is equivalent to:

Int tmp2(24);
return std::move(tmp2);

So your output means:

(0x7ffd8f7693f8) 3 constructed. //i
(0x7ffd8f7693fc) 8 constructed. //j
---
(0x7ffd8f7693c4) 24 constructed. //tmp2 inside operator*
(0x7ffd8f7693c4 -> 0x7ffd8f769404) 24 moved. //return value into tmp1
(0x7ffd8f7693c4) 24 destructed. //tmp2
(0x7ffd8f769404 -> 0x7ffd8f769400) 24 copied. //k = tmp1
(0x7ffd8f769404) 24 destructed. //tmp1
---
(0x7ffd8f769400) 24 destructed. //k
(0x7ffd8f7693fc) 8 destructed. //j
(0x7ffd8f7693f8) 3 destructed. //i
rodrigo
  • 94,151
  • 12
  • 143
  • 190