简体   繁体   中英

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!

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

No, that isn't true, but it's not really your fault: this is confusing.

T obj2 = obj1 is not an assignment, but an initialisation .

So you're right that obj1 will be created first (in your case, a temporary), but obj2 will be constructed using the copy constructor , not default constructed then assigned-to.


For Rectangle C = new Rectangle(3, 4); , the only difference is that the memory space of the temporary variable is allocated to the heap instead of stack.

No, it won't compile, but Rectangle* C would. There are plenty of explanations of dynamic allocation here already.

In this case, there's a significant difference between what actually happens, and what theoretically sort of happens.

The first is simple: Rectangle A(3, 4); just constructs a Rectangle with width and height initialized to 3 and 4 . It's all done in "one step" using the Rectangle(int, int); constructor you defined. Simple and straightforward--so it's what's generally preferred and recommended when it's possible.

Then let's consider: Rectangle B = Rectangle(3,4); . In theory, this constructs a temporary object, then copy-constructs B from that temporary.

In practice, what happens is that the compiler checks that it would be able to create the temporary object, and that it would be able to use the copy constructor to initialize B from that temporary. Having checked that this would be possible, nearly any competent compiler (at least when optimization is enabled, and often even when it's not) will generate code essentially identical to what it did for creating A .

If, however, you delete the copy constructor, by adding:

Rectangle(Rectangle const &) = delete;

...then the compiler will find that it can't copy construct B from the temporary, and will refuse to compile the code. Even if the final, generated code never actually uses the copy constructor, it has to be available for this to work.

Finally, let's look at your third example (with the syntax corrected):

Rectangle *C = new Rectangle(3, 4);

Despite looking somewhat like the line above that created B , the construction involved is really much more like the previous one that you used to create A . Only one object is (even theoretically) created. It's allocated from the free store and directly initialized using your Rectangle(int, int); constructor.

Then the address of that object is used to initialize C (which is only a pointer). Like the initialization of A , this will work even if Rectangle 's copy constructor is deleted, because even in theory there's no copying of a Rectangle involved.

None of these involves an assignment operator. If you were to delete the assignment operator:

Rectangle &operator=(Rectangle const &) = delete;

...all this code would still be fine, because (despite the use of the = ) there's no assignment anywhere in the code.

To use an assignment (if you really insisted on doing so) you could (for example) do something like:

Rectangle A(3, 4);
Rectangle B = Rectangle(5, 6);
B = A;

This still uses only constructors to create and initialize A and B , but then uses the assignment operator to assign the value of A to B . In this case, if you deleted the assignment operator as shown above, the code would fail (would not compile).

Some of our misunderstanding seems to stem from the fact that the compiler with automatically create "special member functions" for you if you don't do something to prevent it from doing so. One of the ways you can prevent it from doing so is the = delete; syntax shown above, but that's not the only one. For example, if your class includes a member variable of reference type, the compiler won't create an assignment operator for you. If you start with something simple like this:

struct Rectangle { 
    int width, height;
};

...the compiler will generate a default constructor, copy constructor, move constructor, copy assignment and move assignment operators, all entirely automatically.

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 .

  1. Rectangle* C = new Rectangle(3,4); , there's grammar error in your code.
  2. operator =(Rectangle& rho) is by-default defined returning reference. Thus, Rectangle B = Rectangle(3, 4); won't create tmp object as you described above.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM