0

I was doing some experiments to see when copy is performed apart from copy elision, RVO, NRVO cases.

So I've written some code like this:

class X {
 public:
  X() { std::cout << "Default constructor" << std::endl; }

  X(const X&) { std::cout << "Copy constructor" << std::endl; }

  X(X&&) { std::cout << "Move constructor" << std::endl; }

  X& operator=(const X) {
    std::cout << "Assignment operator" << std::endl;
    return *this;
  }

  X& operator=(X&&) {
    std::cout << "Move assignment operator" << std::endl;
    return *this;
  }

  ~X() { std::cout << "Destructor" << std::endl; }
};

class Y {
 private:
  X x;

 public:
  const X& getX() const {
    std::cout << "getX" << std::endl;
    return x;
  }
};

int main() {
  Y y;
  std::cout << "assign to ref" << std::endl;
  const X& x1 = y.getX();
  (void)x1;
  std::cout << "assign to const" << std::endl;
  const X x2 = y.getX();
  return 0;
}

and I receive the following as output:

Default constructor
assign to ref
getX
assign to const
getX
Copy constructor
Destructor
Destructor

Both when compiled with gcc or clang with -O3 and tried -std=c++{11,14,17} all produced the same output.

Which surprised me was, I wasn't expecting any copy to be performed when using y.getX(); to a const variable. It is something I used frequently just to ease my access to that variable and its members in the following code, but I wasn't doing it over a const reference instead I was just using const hoping the compiler would regard it just as a renaming.

Does anyone knows why exactly is that copy performed? Only reason that comes to my mind is that it is to make code thread-safe. If there are multiple threads working with object y, then my assignment to const would not be that const after all. Since it would just reference the member x in object y. Which might be changed by other threads. But I am not sure whether that's the real intention or not.

  • In C++, you get what you asked for. If you define an oblect and initialize it by copy, you get two identical objects, not some handle to an existing object. – Quentin Dec 25 '17 at 13:16
  • Well, maybe you just forgot the `&`? This way you ask for and get a copy, not a reference. A `const X&` would again work the way you expect. – Rene Dec 25 '17 at 13:17
  • Actually I was expecting the same behavior from both. As you can see I tested them both, I was pretty sure the const reference one wouldn't make a copy, but can't be sure about why the just const one makes a copy as well. Because it is supposed to be a read-only access to the member variable in y. – Bac Durler Dec 25 '17 at 13:36
  • No, it is supposed to be a new object, which is identical to the original, and happens to be `const`. Programming would be madness if objects could appear and disappear and change lifetimes implicitly depending on the code around them and what the optimizer figures out. – Quentin Dec 25 '17 at 13:45
  • @Quentin *"Programming would be madness if objects could appear and disappear and change lifetimes implicitly depending on the code around them and what the optimizer figures out."* Uh, RVO does precisely that. – Igor Tandetnik Dec 25 '17 at 15:02
  • @IgorTandetnik it does, but in a handful of very specific cases. If RVO actually changes your program's behaviour, then you have done something very nasty with your object's copy/move constructors and destructor, beyond what you're *supposed* to do with them. That's the developper shooting himself in the foot, not the language semantics slipping under him. – Quentin Dec 25 '17 at 15:09
  • Would you expect `&x2 == &y.x`? These are two separate objects at two separate addresses, so *some* constructor must have run to initialize `x2`. – Igor Tandetnik Dec 25 '17 at 15:15
  • @Igor, Acually when you put it that way it makes sense to have a different object. Thanks! – Bac Durler Dec 26 '17 at 10:17

1 Answers1

0

To see the effect of RVO verses compiler forced use of NRVO, play with -fno-elide-constructors compiler switch on the following modified program below. With the usual options you get:

Default constructor 1
assign to ref
getX (with id: 1)
x1 (id:1)
assign to const
getX (with id: 1)
Copy constructor 2
x2 (id:2)
make_X copy
Default constructor 3
make_X (with id: 3)
x3 (id:3)
make_X ref
Default constructor 4
make_X (with id: 4)
x4 (id:4)
Destructor 4
Destructor 3
Destructor 2
Destructor 1

But with NRVO you get:

Default constructor 1
assign to ref
getX (with id: 1)
x1 (id:1)
assign to const
getX (with id: 1)
Copy constructor 2
x2 (id:2)
additional 1
Default constructor 3
make_X (with id: 3)
Move constructor 4
Destructor 3
Move constructor 5
Destructor 4
x3 (id:5)
additional 2
Default constructor 6
make_X (with id: 6)
Move constructor 7
Destructor 6
x4 (id:7)
Destructor 7
Destructor 5
Destructor 2
Destructor 1

Code example:

#include <iostream>
int global_id;
class X {
public:
    X() : id(++global_id) {
        std::cout << "Default constructor " << id << std::endl;
    }
    X(const X&) : id(++global_id) {
        std::cout << "Copy constructor " << id << std::endl;
    }
    X(X&&) : id(++global_id) {
        std::cout << "Move constructor " << id << std::endl;
    }
    X& operator=(const X&) {
        std::cout << "Assignment operator " << id << std::endl;
        return *this;
    }
    X& operator=(X&&) {
        std::cout << "Move assignment operator " << id << std::endl;
        return *this;
    }
    ~X() {
        std::cout << "Destructor " << id << std::endl;
    }
    int id;
};

class Y {
    X x;
public:
    const X& getX() const {
        std::cout << "getX (with id: " << x.id << ')' << std::endl;
        return x;
    }
    X make_X() const {
        X extra;
        std::cout << "make_X (with id: " << extra.id << ')' << std::endl;
        return extra;
    }
};

int main()
{
    Y y;
    std::cout << "assign to ref" << std::endl;
    const X& x1 = y.getX();
    std::cout << "x1 (id:" << x1.id << ")\n";
    (void) x1;
    std::cout << "assign to const" << std::endl;
    const X x2 = y.getX();
    std::cout << "x2 (id:" << x2.id << ")\n";
    std::cout << "make_X copy" << std::endl;
    const X x3 = y.make_X();
    std::cout << "x3 (id:" << x3.id << ")\n";
    std::cout << "make_X ref" << std::endl;
    const X& x4 = y.make_X();
    std::cout << "x4 (id:" << x4.id << ")\n";
    return 0;
}

As you see, the RVO really only comes to play with local variables.

Bo R
  • 2,334
  • 1
  • 9
  • 17