Search code examples
c++classinitializationassignment-operator

Is Rectangle A = Rectangle(3, 4); equivalent to Rectangle A(3,4);?


Below is my code:

#include <iostream>
using namespace std;

class Rectangle {
  int width, height;
 public:
  Rectangle(int, int);
  int area() { return (width * height); }
};

Rectangle::Rectangle(int a, int b) {

  width = a;
  height = b;
}

int main() {
  Rectangle A(3, 4);
  Rectangle B = Rectange(3,4);
  return 0;
}

I didn't define any copy constructors or assignment operator for Rectangle class.

Is it true that Rectangle B = Rectangle(3, 4); actually does three things in serial?

  1. Allocate memory space for a temporary variable (let's use tmp to denote it) of Rectangle, call Rectangle::Rectangle(3, 4) to intialize it.

  2. Allocate memory space for variable B, initialize it with the default constructor

  3. (memberwise) Copy tmp to B with the assignment operator Rectangle& operator = (const Rectangle &)

Does that explanation make sense? I think I might understand wrongly because this process looks very clumsy and inefficient compared with Rectangle A(3, 4);.

Does anyone have ideas about this? Is it true that Rectangle A(3,4) is equivalent to Rectangle A = Rectangle(3, 4);? Thanks!


Solution

  • Rectangle A(3, 4); always simply invokes the Rectangle(int, int) constructor, throughout all of the history of C++ having constructors. Simple. Boring.

    Now for the fun part.

    C++17

    In the newest (as of this writing) edition of the standard, Rectangle B = Rectangle(3,4); immediately collapses down into Rectangle B(3,4); This happens regardless of what the nature of Rectangle's move or copy constructors is. This feature is commonly referred to as guaranteed copy elision, although it's important to stress that there is no copy and no move here. What happens is B is direct-initialized from (3,4).

    pre-C++17

    Before C++17, there is a temporary Rectangle constructed that a compiler may optimize away (and by may, I mean it will certainly, unless you tell it not to). But your ordering of events is not correct. It's important to note that no assignment happens here. We are not assigning to B. We are constructing B. Code of the form:

    T var = expr;
    

    is copy-initialization, it is not copy-assignment. Hence, we do two things:

    1. We construct a temporary Rectangle using the Rectangle(int, int) constructor
    2. That temporary binds directly to the reference in the implicitly generated move (or copy, pre-C++11) constructor, which is then invoked - doing a member-wise move (or copy) from the temporary. (Or, more precisely, overload resolution selects the best Rectangle constructor given a prvalue of type Rectangle)
    3. The temporary is destroyed at the statement.

    If the move constructor is deleted (or, pre-C++11, the copy constructor is marked private), then trying to construct B in this way is ill-formed. If the special member functions are left alone (as they are in this example), the two declarations of A of B will certainly compile to the same code.


    The form of initialization in B may look more familiar if you actually drop the type:

    auto B = Rectangle(3,4);
    

    This is the so-called AAA (Almost Always Auto) style of declarations that Herb Sutter is a fan of. This does exactly the same thing as Rectangle B = Rectangle(3, 4), just with the leading step of first deducing the type of B as Rectangle.