In C++ you will find several ways to initialize an object instance. For example, think about a class “MyClass” which can be constructed with a parameter. The object initialization can be done in several ways:
- MyClass x{y};
- MyClass x(y);
- MyClass x = y;
- MyClass x = {y};
But which one should be used? Do they all call the same constructor (ctor) or do these initializations lead to several results? Within the next two articles I want to think about these questions. The first article will show the several types of possible constructors and the second article will show the ways to initialize object instances.
Example object
For this article we want to create a simple class. An important task of a class is the resource management. So, the example class will contain a dynamically created memory resource which should be created by the different ctor types and released by the destructor (dtor).
Default ctor
Let’s start with the default ctor and the dtor. The default ctor does not have any parameters and it is used to initialize the class internal members.
<h1>include "stdafx.h"</h1> <h1>include </h1> class MyClass { public: MyClass(); // default ctor ~MyClass(); // dtor private: int mSize; int* mElements; }; MyClass::MyClass() : mSize(0) , mElements(nullptr) { std::cout << "default ctor" << std::endl; } MyClass::~MyClass() { if (mElements) { delete[] mElements; mElements = nullptr; } } int main() { MyClass test1; // default ctor <pre><code>return 0;</code></pre> }
Parameterized ctor
If we want to initialize the internal members with variable parameters, we can use a parameterized ctor. For example we can add a parameterized ctor to set the initial size of the data container.
<h1>include "stdafx.h"</h1> <h1>include </h1> class MyClass { public: MyClass(); // default ctor ~MyClass(); // dtor <pre><code>MyClass(const int size); // parameterized ctor</code></pre> private: int mSize; int* mElements; }; MyClass::MyClass(const int size) : mSize(size) , mElements(mSize ? new int[mSize]() : nullptr) { std::cout << "parameterized ctor" << std::endl; }
Copy ctor and copy assignment operator
Another often needed functionality is to create a copy of an existing object. This can be done by using a copy constructor. Furthermore, a copy assignment should be provided as a developer may use both ways to copy an object: copy it during creation or copy it by an assignment.
<h1>include "stdafx.h"</h1> <h1>include </h1> <h1>include </h1> class MyClass { public: MyClass(); // default ctor ~MyClass(); // dtor <pre><code>MyClass(const int size); // parameterized ctor MyClass(const MyClass& obj); // copy ctor MyClass& operator=(const MyClass& obj); // copy assignment operator</code></pre> private: int mSize; int* mElements; }; MyClass::MyClass(const MyClass& obj) : mSize(obj.mSize) , mElements(new int[obj.mSize]) { std::cout << "copy ctor" << std::endl; <pre><code>// create deep copy std::copy(obj.mElements, obj.mElements + mSize, stdext::make_checked_array_iterator(mElements, mSize));</code></pre> } MyClass& MyClass::operator=(const MyClass& obj) { std::cout << "copy assignment operator" << std::endl; <pre><code>// Self-assignment detection if (this == &obj) { return *this; } // release resources if (mElements) { delete[] mElements; mElements = nullptr; } // create deep copy mSize = obj.mSize; mElements = mSize ? new int[mSize]() : nullptr; std::copy(obj.mElements, obj.mElements + mSize, stdext::make_checked_array_iterator(mElements, mSize)); return *this;</code></pre> }
As you can see, things become more difficult now. If we copy an object we must pay attention to several thins. At first, we should decide whether we want to create a deep or a flat copy. At next we must think about the resource management. This can be seen in the implementation of the copy assignment operator. It contains a self-assignment detection and prior to the resource creation we should releases the existing resources.
Move ctor and move assignment operator
The move ctor and operator should create a copy too. But in contrast to the copy operator we get an r-value as parameter and know that the source object is a temporary object only and will no longer be used. This will allow a more efficient resource management. As the source object in no longer used we can steal its resources instead of creating new ones.
<h1>include "stdafx.h"</h1> <h1>include </h1> <h1>include </h1> class MyClass { public: MyClass(); // default ctor ~MyClass(); // dtor <pre><code>MyClass(const int size); // parameterized ctor MyClass(const MyClass& obj); // copy ctor MyClass& operator=(const MyClass& obj); // copy assignment operator MyClass(MyClass&& obj); // move ctor MyClass& operator=(MyClass&& obj); // move assignment operator</code></pre> private: int mSize; int* mElements; }; MyClass::MyClass(MyClass&& obj) { std::cout << "move ctor" << std::endl; <pre><code>// steal content of other object mSize = obj.mSize; mElements = obj.mElements; // release content of other object obj.mSize = 0; obj.mElements = nullptr;</code></pre> } MyClass& MyClass::operator=(MyClass&& obj) { std::cout << "move assignment operator" << std::endl; <pre><code>// Self-assignment detection if (this == &obj) { return *this; } // release resources if (mElements) { delete[] mElements; mElements = nullptr; } // steal content of other object mSize = obj.mSize; mElements = obj.mElements; // release content of other object obj.mSize = 0; obj.mElements = nullptr; return *this;</code></pre> }
Again, we will add a self-assignment detection and release old resources. Furthermore, we have to reset the resources of the source object after we stole them. This will prevent the source object dtor to release these resources.
Copy & swap idiom
The copy ctor and the copy assignment operator as well as the move ctor and the move assignment operator contain some duplicate source code. There exists a common implementation technique which addresses this issue: the copy & swap idiom. This implementation technique comes with the advantage to remove this duplicate code but it will have some disadvantages too. Within this article I don’t want to explain the copy & swap idiom because it’s an own complex topic but you should keep in mind that this idiom is existing.
Initializer-list
Another important ctor type, especially for container like object, is a ctor with an initializer list. This will allow to pass an array of objects which is used to initialize the container class.
<h1>include "stdafx.h"</h1> <h1>include </h1> <h1>include </h1> <h1>include </h1> class MyClass { public: MyClass(); // default ctor ~MyClass(); // dtor <pre><code>MyClass(const int size); // parameterized ctor MyClass(const MyClass& obj); // copy ctor MyClass& operator=(const MyClass& obj); // copy assignment operator MyClass(MyClass&& obj); // move ctor MyClass& operator=(MyClass&& obj); // move assignment operator MyClass(const std::initializer_list<int>& list); //initializer_list ctor</code></pre> private: int mSize; int* mElements; }; MyClass::MyClass(const std::initializer_list& list) : mSize(list.size()) , mElements(mSize ? new int[mSize]() : nullptr) { std::cout << "initializer_list ctor" << std::endl; <pre><code>if (list.size()) { std::copy(list.begin(), list.end(), stdext::make_checked_array_iterator(mElements, mSize)); }</code></pre> }
Use the different ctor types
The following source code contains an example console application which will create object instances. Depending on the given parameters the according ctor type is called.
int main() { MyClass test1; // default ctor <pre><code>MyClass test2(7); // parameterized ctor MyClass test3(test1); // copy ctor test3 = test1; // copy assignment operator MyClass test4(std::move(test1)); // move ctor test4 = std::move(test2); // move assignment operator MyClass test5(7.1); // parameterized ctor, warning: conversion from double to int MyClass test6{ 1,2,3,4,5 }; //initializer_list ctor return 0;</code></pre> }
Implicit conversion ctor vs. explicit ctor
So far, we have implemented a couple of ctor’s for MyClass. With that ctor’s in mind do you think the following line of code will construct an object instance or will it show an error? If an object instance is created, which type of ctor is used?
MyClass test = 7;
This line of code will create a MyClass instance by calling the parameterized ctor. The parameterized ctor is also called a “conversion” ctor because it will allow implicit type conversion. In our case the MyClass instance is created based on an int value, so you can say the int value will implicit converted to an MyClass by calling the according parameterized ctor.
But maybe you don’t want to support such an implicit conversion. That may have several reasons. In my opinion such an implicit conversion looks a little bit strange and there are a lot of developers which don’t know the technical background about this line of code. Most will know that an object is created but some don’t know which ctor is used or whether it is a combination of ctor and assignment. This uncertainty and side effects on code changes may results in errors too. Therefore, you may prevent implicit conversion for some kinds of classes. In this case you have the possibility to declare the parameterized ctor as explicit. If you do so, the ctor cannot be used as implicit conversion ctor. The following source code shows an according example.
<h1>include "stdafx.h"</h1> <h1>include </h1> <h1>include </h1> class MyClass1 { public: MyClass1() { std::cout << "default ctor" << std::endl; }; MyClass1(const int size) { std::cout << "parameterized ctor" << std::endl; }; MyClass1(const MyClass1& obj) { std::cout << "copy ctor" << std::endl; }; }; class MyClass2 { public: explicit MyClass2() { std::cout << "default ctor" << std::endl; }; explicit MyClass2(const int size) { std::cout << "parameterized ctor" << std::endl; }; explicit MyClass2(const MyClass1& obj) { std::cout << "copy ctor" << std::endl; }; }; int main() { MyClass1 test1 = 7; // OK; calls parameterized ctor MyClass1 test2 = 7.1; // OK; calls parameterized ctor, warning: conversion from double to int <pre><code>// MyClass2 test3 = 7; // ERROR; implicit conversion from int to MyClass2 is not allowed // MyClass2 test4 = 7.1; // ERROR; implicit conversion from double to MyClass2 is not allowed MyClass2 test6 = MyClass2(7); // OK; calls parameterized ctor MyClass2 test7 = MyClass2(7.1); // OK; calls parameterized ctor, warning: conversion from double to int return 0;</code></pre> }
MyClass2 offers an explicit ctor only. Therefore, the object instantiation cannot be done by using implicit conversion. Instead the parameterized ctor must be used explicitly. This may result in cleaner source code and may prevent errors.
Summary and outlook
Within this article we have seen the different types of constructors and got an introduction how to use them, for example to manage the resources needed by the object instance. Within the next article we will see the different ways to create an object and see which ctor is used in which situation.