As I wrote in my last post, How Best to Learn a Programming Language I am currently trying to learn modern C++. With most of my experience rooted in Unity and C#, coming back to C++ has been a somewhat challenging experience.
To help reinforce my understanding of the language better, I am writing articles about some of the more challenging, confusing and interesting aspects of C++ I have come across whilst studying the language. If I am successful, then hopefully these articles will help anyone else that reads them.
To start off with lets look at the difference between copy and direct initialisation. There are many types of initialisation in C++, which you can explore here but for this article let’s focus on two common forms, copy and direct initialisation, and how they differ. Before all of that though let's make sure we understand what we mean by initialisation.
What is Initialisation?
When we talk about initialisation, we are referring to when we construct a variable and assign it an initial value.
// construct the variable of type s1 and initialise it with
// the value "Hello"
string s = "Hello";
When we initialise a variable, the compiler will call one of the constructors available for the type we are trying to initialise. This should be familiar to anyone who has used an object-orientated language but its good to make sure we are all on the same page.
With that brief explanation out of the way let's look at copy initialisation.
Copy Initialisation
In C++ copy initialisation happens when we use the assignment operator "=" to initialise a variable.
string s = "Hello";
What happens in the above call is that the variable s
is constructed and the compiler then looks at the constructors in string
to determine if any can accept the value "Hello"
. If a constructor is found, which happens in this example, then the value is passed into the constructor of the object and is used as part of initialisation.
Direct Initialisation
Direct initialisation is an alternative to copy initialisation which uses braces ()
to initialise a variable, instead of using the assignment operator.
string s("Hello");
Outside of the difference in operators used to initialise s
the compiler looks for the best matching constructor and calls it, passing in "Hello"
. Again we end up producing a variable which is set up in the same way as if we used copy initialisation.
As you might have guessed, although both forms look identical, there is a difference between copy and direct initialisation.
What is the Difference between Direct vs Copy Initialisation?
The fundamental difference between the two-forms of initialisation has nothing to do with the result of the initialisation but with selecting the best matching constructor to use.
When using direct initialisation the compiler also includes any explicit constructors the class might have but copy initialisation ignores them.
What is an explicit constructor?
An explicit constructor is a constructor we define using the explicit
keyword, and it is useful if we want to stop the compiler from performing implicit conversions.
What is an Implicit Conversion?
An implicit conversion is when the compiler converts one type into another type without us knowing, to usually make our lives easier. A great example of this trying to add two different types together, such as an int
and double
. In this example the compiler implicitly converts the int
into a double
.
double a = 0.0;
int b = 3;
// convert b to a double and then convert the result of a + b
// to an int
int c = a + b;
The compiler doesn't just implicitly convert built-in types, it can also perform implicit conversions on types we define ourselves. An implicit conversion is actually happening in the copy initialisation section were we initialise the variable s
with "Hello"
.
The value "Hello"
is actually an array of const char
and not a string
so string
has to implement a converting constructor in order to allow a string
object to be constructed from this array of characters.
// implicitly convert const char array "Hello" to string using
// the string types conversion constructor
string s1 = "hello";
One problem with implicit conversions is that you might not know they are happening. It also hides information you might need to know, like in the above example that a string literal is not actually a string
.
Stopping Implicit Conversions with Explicit Constructors
To stop the compiler from performing an implicit conversion we can mark a converting constructor as explicit using the explicit
keyword. When we mark the constructor as explicit we can no longer use copy initialisation and must either perform direct initialisation, or use an explicit cast.
class A
{
public:
// Conversion constructor
A(int num)
{
std::cout << "int constructor" << std::endl;
}
// Copy constructor
A(const A& other)
{
std::cout << "copy constructor" << std::endl;
}
};
int main()
{
// copy initialisation - error no viable constructor
// as the compiler ignores explicit constructors when
// performing an implicit conversion
A a1 = 4;
// copy initialisation - okay as we perform an explicit
// cast and this ensures the explicit constructor is
// considered
A a2 = static_cast<A>(3);
// direct initialisation - okay as direct initialisation
// considers explicit constructors
A a2(4); // okay using direct initialisation
return 0;
}
In the above example we have a simple class A
that has a conversion constructor and a copy constructor. We then try to initialise several variables using both copy and direct initialisation. Since we create a converting constructor that takes an int
we can convert an int
into our type A
. It marked the compiler as explicit
to stop the compiler from performing implicit conversions. This means that we can only use that constructor for initialisation when we either use an explicit cast or use direct initialisation.
What this Means in Practice
In practice, direct and copy initialisation are very similar despite the differences in syntax. They both try to find the best matching constructor in order to initialise a variable at construction.
The key difference is that copy initialisation allows for implicit conversions to happen and ignores explicit constructors. This means that if you want an explicit constructor to be considered when using copy initialisation you need to perform an explicit cast or just use direct initialisation.