Eyal
Eyal

Reputation: 409

C++ copy constructor - small but important difference

I couldn't figure out what is happening here, thought it is very strange, and after getting to understand the reason I thought sharing the answer would be valuable to somebody's time.

So given this simple code:

    #include <iostream>
using namespace std;

class Shape {
public:
    int* a;
    Shape(){
        cout<<"Default Shape constructor"<<endl;
        a = new int(8); // default
    }
    Shape(int n){
        a = new int(n);
          cout<<"Shape(n) constructor"<<endl;
    }
    // copy constructor
    Shape(const Shape& s){
        cout<<"Shape copy constructor"<<endl;
        a = new int(*(s.a));
    }
    Shape& operator=(const Shape& s){
        cout<<"Shape operator="<<endl;

        if (&s == (this))
            return (*this);
//      this.clear();
        a = new int(*(s.a));

        return (*this);
    }


      virtual void draw(){
             cout<<"Print Shape the number is "<<*a<<endl;
      };
      virtual ~Shape(){
          delete a;
          cout<<"Shape distructor"<<endl;
      }
};

class Circle : public Shape {
public:
    int b;
  Circle() {
      cout<<"Default Circle constructor"<<endl;
      b=0;
  }
  virtual void draw() {
      cout<<"Printing Circle. The number is "<<b<<endl;
  }
   ~Circle(){
      cout<<"Circle distructor"<<endl;
    }
};

Why is the two following tests gives two different answers:

static void test1(){
    Shape shape = Circle() ;
    shape.draw();
}

static void test2(){
    Shape* shape = new Circle() ;
    shape->draw();
            delete shape;
}

Well, because I'm getting to know the virtual mechanism just now, I figured that both test will produce the same result (printing Circle). While this is what happens in test2 it is not the case in test1.

To understand why, I wrote what really happens in the backround.

Test1: 1. The program perform the line " Circle() ". 1.1 a call to the default constructor of Shape is made (because Circle is derived from Shape). 1.2 a call to the default constructor of Circle is made.

  1. The program performs the action " Shape shape = ". This actually calls to the copy constructor of Shape. * here you should note that the copy constructor does not copy the _vptr that is an invisible field in Circle. It only copies the value of a and returns (*this). This is the real reason why it doesn't print Circle.

Here I do have another question. When ran test1 I got this output: Default Shape constructor Default Circle constructor Shape copy constructor Circle distructor Shape distructor Print Shape the number is 8 Shape distructor

If the copy constructor signature is Shape(const Shape& s), according to this output, there is a call to copy constructor before actually creating shape as Shape. How can this happen?

Test2: 1. A new instance of class Circle is being build on the heap. (The line new Circle is performed) 2. A pointer to that address in memory on the heap is returned and placed in the pointer shape. In the first four bytes of this address lies the pointer to the virtual table of Circle. That is why test1 is different from test2.

It is important to understand that the the difference between the test has nothing to do with the fact that test1 builds a Circle on the stack and test2 builds a Circle on the heap. Well, actually it has something to do with it. But the real reason is that the copy constructor does not copy the _vptr.

Upvotes: 4

Views: 1835

Answers (4)

Sarfaraz Nawaz
Sarfaraz Nawaz

Reputation: 361612

As others already have pointed out the problem with your code, as to why you wouldn't get same result for both test functions, I've something else to say which you can experiment with to have better understanding of how virtual mechanism works.

Since you already have used pointers to achieve runtime polymorphism, lets experiment with references now. See my modification is test1():

static void test1(){
    Circle circle;
    Shape & shape = circle;  //note &
    shape.draw();
}

static void test2(){
    Shape* shape = new Circle() ;
    shape->draw();
    delete shape;
}

Now both these function would print same thing.

The bottomline is : in C++, runtime polymorphism is achieve only through pointers and references whose static type is base class, and dynamic type is the object that it points/refers to.

Lets do more experiment:

  Circle circle;
  circle.draw();

  Shape & s1 = circle;
  s1.draw();

  Shape & s2 = s1;
  s2.draw();

  Shape & s3 = s2;
  s3.draw();

What would s2.draw() and s3.draw() do? The answer is : they would do the same thing as s1.draw() and circle.draw() would do. Means, all of them would call Circle::draw(), none would call Shape::draw().

Upvotes: 0

James McNellis
James McNellis

Reputation: 355187

Shape shape = Circle();

There is no assignment here and thus there is no call to the assignment operator. The = used here is initialization. A temporary Circle object is created by Circle() then its Shape part of that temporary object is copy constructed into shape. The temporary object is then destroyed because it is no longer needed.

Shape* shape = new Circle();

A Circle object is created dynamically (on the heap) and a pointer to that object is returned. The shape pointer points to the Shape part of this Circle object. "The vptr not getting copied" is not the cause of the difference, it's an effect. You've written two tests that do two completely different things so you get to completely different results. The "different vptr" is merely one result that is different.

You should almost never need to worry about low-level implementation details like "the vptr" and related things when programming in C++. It should be possible to reason about the code at the language level, and only concern yourself with implementation details when investigating performance and when debugging the ugliest of issues.

Upvotes: 6

Ben Voigt
Ben Voigt

Reputation: 283733

I don't know why you think operator= is called before shape is constructed -- in fact operator= is never called.

There is no vptr anywhere in the C++ standard. The real reason that virtual members called on Shape shape act as if shape is a Shape and not a Circle is that shape really is not a Circle and never was. The C++ standard requires it to be this way. Shape shape doesn't have any members of Circle, there is no space allocated for the data members of Circle, and it would be rather crazy to try to use the virtual functions when their data doesn't exist.

Shape shape creates an instance of Shape, no matter how it is initialized.

Upvotes: 0

sehe
sehe

Reputation: 393467

It's called 'slicing' the class by copying (non-polymorphically) to the base type

See Thinking in C++ for backgrounder

Upvotes: 8

Related Questions