Saturday, August 13, 2016

Transition from C to C++: Class Constructors (C++98)

In this post, I will outline what a constructor does in C++ class.

Basically, a constructor is called when an object is instantiated or created. A constructor differs from a method in that it does NOT return anything, not even void. There are some notable constructors that are very important:
1. Default constructor: takes no argument.
2. Copy constructor: takes an object of the same class as an argument

These constructors are automatically generated by the compiler if not provided explicitly by the programmer. Let's consider an example below:



#include <iostream>

class Foo {
public:
  int data;
};

using namespace std;

int main() {
  Foo obj1; // default constructor
  Foo obj2 = {1}; // aggregate initialization
  Foo obj3(obj2); // default copy constructor
  Foo obj4 = obj3; // default copy constructor
  Foo obj5; // default constructor
  obj5 = obj2; // copy operator
  cout << obj1.data << endl;
  cout << obj2.data << endl;
  cout << obj3.data << endl;
  cout << obj4.data << endl;
  cout << obj5.data << endl;
  return 0;
}

/* output
129319203
1
1
1
1
*/
Because no constructor has been explicitly declared, both the default constructor and copy constructor have been auto-generated. For the implicit default auto-generated constructor, the fields are not initialized, so we will get some random value for obj1.data, as line 11 calls this implicit default constructor.

Line 12 is not calling the constructor; rather, this is simply aggregate initialization that we have seen in C.

Line 13 calls the copy constructor, but since it is not explicitly declared, the compiler will invoke implicit auto-generated copy constructor that simply copies the entire fields.

Line 14 also calls the copy constructor. It may be confusing, but line 16 does not call copy constructor; instead, it calls copy operator. The difference is whether the object is being declared the first time or not. In line 14, it is first declared, so the constructor needs to be invoked. Since the argument is another object of the same class, copy constructor is invoked. On the other hand, in line 16, the constructor needs not be called, since the default constructor has already been called in line 15. I will go over the copy operator in the next post. For now, it suffices to say that the copy operator is a special type of class method.

Now, let's consider the same code with explicitly-declared default constructor:


#include <iostream>

class Foo {
public:
  int data;
  /* default constructor: init data to zero */
  Foo() : data(0) {}
};

using namespace std;

int main() {
  Foo obj1; // default constructor
//  Foo obj2 = {1}; // aggregate initialization cannot be used when any constructor is explicitly declared
  Foo obj2;
  obj2.data = 1;
  Foo obj3(obj2); // default copy constructor
  Foo obj4 = obj3; // default copy constructor
  Foo obj5; // default constructor
  obj5 = obj2; // copy operator
  cout << obj1.data << endl;
  cout << obj2.data << endl;
  cout << obj3.data << endl;
  cout << obj4.data << endl;
  cout << obj5.data << endl;
  return 0;
}

/* output
0
1
1
1
1
*/

The default constructor is declared in line 7. It simply initializes data field to be zero. Note that it does not have a return type, since it is not returning anything. Also, a constructor is usually declared with public access, since it will be invoked from an external entity. data(0) will set data as 0 by calling the appropriate class's constructor. In this case, it is a simple int primitive data, so it should be equivalent to data = 0. If data type is a class, it will invoke corresponding constructor, as we can see in the next example below:



#include <iostream>

class Bar {
public:
  int i;
  Bar(int i) : i(i) {
  }
};

class Foo {
public:
  int data;
  Bar bar;
  /* default constructor: init data to zero */
  Foo(): bar(3), data(0) {
  }
};

using namespace std;

int main() {
  Foo obj1; // default constructor
  cout << obj1.data << endl;
  cout << obj1.bar.i << endl;
  return 0;
}

/* output
0
3
*/

Here, line 23 calls Foo's default constructor in line 16, which then calls Bar's constructor with an argument in line 7. Note that because Bar's constructor is explicitly declared, the compiler will not auto-generate implicit default constructor.

Finally, the copy constructor shall be defined in the following way:


#include <iostream>

class Bar {
public:
  int i;
  /* constructor with an agrument to init i to */
  Bar(int new_i) : i(new_i) {
  }
};

class Foo {
public:
  int data;
  Bar bar;
  /* default constructor: init data to zero */
  Foo(): bar(3), data(0) {
  }
  /* copy constructor: copy from given object */
  Foo(Foo &other) : data(other.data), bar(other.bar) {}
};

using namespace std;

int main() {
  Foo obj1; // default constructor
  cout << obj1.data << ", " << obj1.bar.i << endl;
  obj1.data = 1;
  Foo obj2(obj1);
  Foo obj3 = obj1;
  cout << obj2.data << ", " << obj2.bar.i << endl;
  cout << obj3.data << ", " << obj3.bar.i << endl;
  return 0;
}

/* output
0, 3
1, 3
1, 3
*/


In line 19, Foo's copy constructor has been explicitly declared. This constructor is invoked from line 28 and 29, and it will in turn invoke Bar's copy constructor. However, since Bar's copy constructor has not been explicitly declared, the compiler generates an implicit copy constructor.

So, one may ask this question: if the implicit copy constructor works well, why should we bother to declare it explicitly? The answer is that sometimes the class's field is a pointer to some external data, and if we rely on the default copy constructor, it will simply copy the pointer, so the two objects will point to the same data. This will defeat the purpose of making a copy, as modifying the data will affect both objects. Hence, one needs to explicitly declare the copy constructor to allocate a copy of the external data and have the field point to this copy. We will cover this scenario in more details in a follow up post.

No comments:

Post a Comment