Whenever you write an application in C++ you will create a lot of object instances. So, this is a base development task. C++ offers several ways to initialize variables. These are not just syntactical variations for the same task. The different initialization kinds may result in different behaviors. This rich variety of initialization kinds can result in some pitfalls, wrong expectations and programming errors.
Within this article I want to show the different object instance creation methods and explain their differences. To fully understand the examples of this article you should know the different types of constructors. You can get or refresh knowledge about constructors within this article.
Example Class
Within the examples of this article, a class “MyClass” is used. As we want to focus on the object instance creation, this class does not have any functionality. But it will provide several standard constructors and assignment operators. The ctor’s and operators just contain console outputs. This will help to see which ctor or operator is called. The following source code shows “MyClass”.
#include "stdafx.h" #include #include #include class MyClass { public: MyClass(); // default ctor ~MyClass(); // dtor 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& list); //initializer_list ctor }; MyClass::MyClass() { std::cout << "default ctor" << std::endl; } MyClass::~MyClass() { } MyClass::MyClass(const int size) { std::cout << "parameterized ctor" << std::endl; } MyClass::MyClass(const MyClass& obj) { std::cout << "copy ctor" << std::endl; } MyClass& MyClass::operator=(const MyClass& obj) { std::cout << "copy assignment operator" << std::endl; return *this; } MyClass::MyClass(MyClass&& obj) { std::cout << "move ctor" << std::endl; } MyClass& MyClass::operator=(MyClass&& obj) { std::cout << "move assignment operator" << std::endl; return *this; } MyClass::MyClass(const std::initializer_list& list) { std::cout << "initializer_list ctor" << std::endl; }
Quiz
As a developer you already have implemented a huge number of object instantiations. Therefore, I want to start with a quiz. Following source code shows several ways to initialize MyClass. Please take a few minutes and think about these initializations. Try to answer following question for each line of code: Which ctor and/or assignment operator is called?
int main() { MyClass test1; MyClass test2(); MyClass test3{}; <pre><code>MyClass test4(42); MyClass test5{ 42 }; MyClass test6(42.5); MyClass test7{ 42.5 }; MyClass test8 = 42; MyClass test9 = 42.5; MyClass test10 = { 42 }; MyClass test11 = { 42.5 }; MyClass test12 = MyClass(); MyClass test13 = MyClass(42); MyClass test14 = MyClass{ 42 }; MyClass test15(test1); MyClass test16{ test1 }; MyClass test17 = test1; MyClass test18 = { test1 }; return 0;</code></pre> }
MyClass test1
This is the simplest way to create an object instance. The default constructor will be called. Within the default constructor you should initialize all class members, otherwise they may contain garbage values.
MyClass test2()
Like before, this looks quite simple and we may expect that the default constructor is called. But not even close. This isn’t an object initialization at all. It is a function declaration. The function “test2” without parameters and a return value “MyClass” is declared. This C++ pitfall results in the redundant use of the parentheses. For backward compatibility the meaning of this code it still as in C++98 so it is still a function declaration. To bypass this pitfall, you should not use this syntax at all. Instead use the version seen above without parenthesis or use the braces syntax introduced with C++11 (as you can see in the next paragraph). But on the other hand, it is not a big issue because it will not result in errors. If you try to use the supposed object instance you will get according errors and if you not use the “test2” you get according compiler warnings too.
MyClass test3{}
This syntax was introduced with C++11. An object initialization with braces “{}” will call the default ctor. So, this syntax is equivalent to “MyClass test1”.
MyClass test4(42)
This will call the parameterized ctor and pass the “42” as parameter. The example class is a container type and therefore this object initialization will provide a container for 42 elements initialized with default value.
MyClass test5{ 42 }
If we use the braces syntax the values inside the braces will be converted to an initializer_list and therefore the initializer_list ctor is called. If we again think about the created container object we can say this time a container with one element was created and the element value was set to 42.
So “MyClass test4(42)” and “MyClass test5{ 42 }” will have different results but the syntax is nearly the same. This is a very important aspect and unfortunately a source for errors. Therefore, we should analyze this topic in more detail.
Furthermore, the braces syntax is still allowed even if we don’t have an initializer_list ctor. In this case the parameterized ctor is called according to the values given inside the braces.
In case a parameterized ctor and an initializer_list ctor exists the initializer_list ctor is prevered and will hide the parameterized ctor. This means if we get such a collision of two possible ctor’s the one with the initializer_list is prevered automatically and we don’t get any compiler warning. This may be a source for errors.
For example, we may use a container class “MyContainer”. This container class offers a parameterized ctor with two parameters: number of elements, initial value. We can create an object instance with “MyContainer x{10, 5}”. This will create a container with 10 elements all initialized with value 5. After a couple of time, the class MyContainer will be extended by the nice feature of an initializer_list ctor. But this new feature will change the behavior of the user code which uses the class. The existing initialization “MyContainer x{10, 5}” will now create a container with two elements of value 10 and 5. To fix this error we have to change the initialization and use parenthesis to call the hidden parameterized ctor: “MyContainer x(10,5)”.
This example shows the issues you may get with the initializer_list ctor. If you add this ctor to an existing class and if you have had parameterized ctor’s so far, they will get hidden and as a result you may break user code.
You will find an according example in the standard template library. The vector class offers an initializer_list ctor and it offers a hidden parameterized ctor with two parameters: number of elements, initial value.
MyClass test6(42.5)
The example class contains a parameterized ctor with an integer value as parameter. This parameterized ctor will be called even if the parameter does not match. An according value conversion is done. This narrowing conversion is allowed for some build in types but it may result in a loss of data and therefore an compiler warning will be shown.
MyClass test7{ 42.5 }
An object instance creation with braces will call the parameterized ctor too. But in contrast to the version above with parentheses syntax, narrowing conversions are not allowed. Therefore, in our example this object instantiation will result in an error.
MyClass test8 = 42
MyClass test9 = 42.5
MyClass test10 = { 42 }
MyClass test11 = { 42.5 }
These initializations are nearly the same as the ones explained above, with the syntactical difference that we use an assignment operator. But what’s the consequence of this different syntax? Will the assignment operator of MyClass be called?
The answer is simple: The use of the assignment operator is just a syntactical difference. These initializations are therefore equal to the ones explained above (see ’test4’ to ‘test7’).
Soo for test8 and test9 the parameterized ctor is called. Test10 will call the initializer_list ctor. Test11 will result in a compiler error as the narrowing conversion is not allowed.
MyClass test12 = MyClass()
MyClass test13 = MyClass(42)
MyClass test14 = MyClass{ 42 }
Now it becomes a little more difficult. What will happen in these cases? If we look at the different parts of the syntax, for example for the first case “MyClass test12 = MyClass()” we may think following: The “MyClass()” command creates an temporary object instance by calling the default ctor and “=“ will call the assignment operator and assign the temporary object to “test12” which was previously created due to the command “MyClass test12”. But this assumption is wrong. Unfortunately, I have heard it a few times, especially when people say you can optimize you code by eliminating the supposed temporary object and the call of several ctor’s and assignments.
So, what’s happening by using this kind of syntax? Nothing special! It has the same meaning as the syntax used for “test1”, “test4“ and “test5”. Therefore, for test12 the default ctor is called, for test13 the parameterized ctor is called and for test15 the initializer_list ctor is used. No temporary object is created and the assignment operator is never called.
In summary of the examples seen so far, we can say there is no difference between the following three initializations which will all call the parameterized ctor. Same is true for the default ctor and initializer_list ctor examples seen so far.
- MyClass test4(42)
- MyClass test8 = 42
- MyClass test13 = MyClass(42)
These three spellings will create an instance of MyClass by calling the parameterized ctor. If you have read the article mentioned at the beginning you will answer back that there may be a theoretical difference. If we use explicit ctor’s the second syntax will no longer allowed. But that’s a restriction for explicit ctor’s only. In terms of common concepts, the three spellings will have the same result. But which one should be preferred? This depends on the coding guidelines of your company, your project team or your personal preferences. At the end of the article I will mention some coding guidelines.
MyClass test15(test1)
MyClass test16{ test1 }
These cases will call the copy ctor. As the given parameter is of type MyClass, the braces will not create an initializer_list.
MyClass test17 = test1
MyClass test18 = { test1 }
And again, the copy ctor will be called. As explained before, even if the syntax suggest that the assignment operator function is involved, it will never be called. So these initializations are nearly equal to the previous ones (test15 and test16) with the small difference that explicit ctor cannot be called.
Summary
As you can see there are many ways to initialize an object. These different initializations could have big differences in syntax but they have the same behavior. But unfortunately, there are some pitfalls to, like the initializer_list ctor which may hide a parameterized ctor. The braces syntax will offer uniform way to initialize objects. It should be used as preferred syntax as it can be used in nearly all cases. Following you will find some guidelines for object initializations but of course you may have your own coding guidelines or preferences.
Guidelines
Prefer object initialization with braces “{…}”, because it’s more consistent, more correct, can be used in nearly all cases and avoids old-style pitfalls at all.
In single-argument cases, especially on initialization of build in types, it is fine to omit the braces, for example “int i = 8;”.
In rare cases use parentheses “(…)” to explicitly call a ctor which is otherwise hidden by an initializer_list ctor.
When you design a class, avoid providing a ctor that ambiguously overloads with an initializer_list ctor. Users of your class should never need to use parentheses syntax to reach such a hidden ctor.