Advertisement
  1. Code
  2. Mobile Development
Code

C++ Succinctly: Constructors, Destructors, and Operators

by
Languages:
This post is part of a series called C++ Succinctly.
C++ Succinctly: Storage Duration
C++ Succinctly: Resources Acquisition Is Initialization

Introduction

C++ constructors are more complicated than their C# counterparts. For one thing, there are five types of constructors. Although you will rarely write all five types for any particular class or structure, you do need to know what they are, what they do, and what they look like. If not, you could find yourself facing some very confusing bugs or compiler errors.

The reason C++ constructors are more involved than C# constructors is the variety in storage duration that a C++ class can have. By default, C++ objects have value semantics while C# classes have reference semantics. Here’s an example:

Let’s assume vehicle is not null but is a valid object of type Vehicle, and that the Vehicle type is a class type as opposed to a structure or something else.

Let’s consider the previous code statement as if it were C# code. In C#, the object that vehicle refers to lives off somewhere in a heap managed by the GC. The previous code statement stores a reference to that object in the someVehicle variable, which it gets from the existing vehicle reference to the object. There is still just the one instance of that object with two references to it.

Let’s now consider the previous statement as if it were C++ code. In C++, the object that vehicle refers to is likely an automatic duration object, but it could be a static duration object or even a thread duration object. The previous code statement would by default create a copy of the vehicle object and store it in the address of the automatic duration someVehicle variable. It does this using something called a copy assignment operator, a close relative of a copy constructor.

There are now two copies where once there was one. Unless the Vehicle class has a pointer, a std::shared_ptr, a reference, or something of that sort as a member variable, the two copies are completely separate. Changing or even destroying one will have no effect whatsoever on the other.

Of course, if you want someVehicle to be a reference to vehicle’s object, you can change the code slightly and accomplish that. But what if you wanted someVehicle to become vehicle and for vehicle to stop existing—to be destroyed without taking its former resources with it? C++ makes this possible through something called a move constructor and a move assignment operator.

If you want to intentionally disable copy semantics (assignment and construction, collectively) or move semantics, that is possible too.


Default Constructor

We’ll start with default constructors. A default constructor can be called with no arguments. A class can have no more than one default constructor. If you define a class and include no constructors, you produce an implicit default constructor, which the compiler creates for you. However, if you have a class member variable that is a class, structure, or union that has no default constructor, then you must define at least one constructor because the compiler cannot create an implicit default constructor.

If you define a constructor with no parameters, it is an explicit default constructor. It is also possible to define a constructor that takes parameters and still make it a default constructor using default arguments, which we will discuss in a moment.

If you define a constructor that has at least one required parameter, then the compiler will not generate a default constructor. You must define an explicit default constructor if you want one.


Default Arguments in Function Declarations

Constructors in C++, and functions in general, can have default arguments specified for some or all of their parameters as part of their declaration (similar to C#’s optional arguments). In C++ functions, all parameters with default arguments must occur to the right of any parameter without a default argument in the function declaration. C++ does not support C#-style named arguments, but you can accomplish something similar using the named parameter idiom.

Why mention default arguments here? Well, it turns out that if you have a constructor in which all the parameters have default arguments, that constructor is a default constructor. It makes sense, since if you have a constructor with the signature Vehicle(VehicleType type = VehicleType::Car, double odometerReading = 0.0); then you can call that constructor with empty parentheses, and those default arguments will be applied. If you define a default constructor, you can have only one, regardless whether it has no parameters, or whether its parameters all have default arguments.

This principle goes further still. No two functions with the same name that are declared in the same scope can have the same parameter types in exactly the same positions. You guessed it: Parameters with default arguments are disregarded for the purpose of distinct function signatures.

It all makes sense because default arguments make it impossible to distinguish between double Add(double a, double b = 0.0); and double Add(double a, int a = 0); given that both could be called as double dbl = Add(5.0);. The compiler cannot know which you intended in that case, so it simply fails to compile and displays an error message.


Parameterized Constructors

A parameterized constructor has one or more parameters. A parameterized constructor where all of the parameters have default arguments is also the default constructor for a class. There is nothing special about parameterized constructors in C++.


Conversion Constructors

A conversion constructor has at least one parameter. If there is more than one, then those additional parameters must have default arguments.

If you do not want a constructor to be a conversion constructor, you can mark it with the function specifier: explicit. Let’s look at an example:

As you can see, the conversion constructor lets us construct s2 by directly setting it equal to a string value. The compiler sees that statement, checks to see if SomeClass has a conversion constructor that will receive that sort of value, and proceeds to call the appropriate SomeClass constructor. If we tried that with the commented-out sc4 line, the complier would fail because we used explicit to tell the compiler that the constructor, which just takes int, should not be treated as a conversion constructor, but instead should be like any other parameterized constructor.

Conversion constructors can be useful, but they can also lead to bugs. For example, you could accidentally create a new object and assign it to an existing variable when you merely mistyped a variable name and really meant an assignment. The compiler won’t complain if there is a valid conversion constructor, but will complain if there isn’t. So keep that in mind and remember to mark single parameter constructors as explicit, except when you have a good reason for providing a conversion constructor.


Initialization of Data and Base Classes

By this point, we have seen quite a few constructors, so it’s important to discuss the strange syntax you’ve encountered. Let’s examine a sample:

Note: This sample uses some very bad code practices in order to illustrate how C++ performs initialization. Specifically, the ordering of initializers in constructor definitions is misleading in some places. Always make sure your constructors order their initialization of base classes and parameters in the order that the initialization will actually occur during program execution. That will help you avoid bugs and make your code easier to debug and easier to follow.

Sample: InitializationSample\InitializationSample.cpp

The first thing we’ve done is define two helper functions that write messages, so we can easily follow the order in which things are happening. You will notice that each of the classes has a constructor that takes int, though only Y and Z put it to use. A, B, C, and D do not even specify a name for the int in their int parameter constructors; they just specify that there is an int parameter. This is perfectly legal C++ code, and we’ve been using it all along with our commented-out parameter names in _pmain.

Class A has two constructors: a default constructor and a parameterized constructor that takes an int. The remaining classes have only parameterized constructors, each of which takes an int.

  • Class B inherits from A virtually.
  • Class C inherits from nothing.
  • Class D inherits from nothing.
  • Class Y inherits from B virtually, from D directly, and from C virtually, in that order.
  • Class Z also inherits from D directly, from B virtually, and from C directly, in that order.

Let’s look at the output this program gives us and then discuss what is happening and why.

In our _pmain function, first we create an object of type Y within its own scope. We then call its GetSomeInt member function. This helps ensure that the compiler will not optimize away the creation of Y in a release build in case you mess around with the code. It also serves as a marker between construction and destruction.

We then exit the scope of Y, triggering its destruction. After this, we write another marker string to separate the instantiation of a Y instance from the instantiation of the Z instance that follows. We create the Z instance in its own scope so we can follow its construction and destruction the same way as with Y.

So what do we see? Quite a lot. Let’s focus on Y first.

When we call the Y constructor, the first thing it does is call the default constructor for A. This might seem terribly wrong for several reasons, but it is, in fact, right. The Y constructor says to follow this order:

  1. Initialize base class C.
  2. Initialize Y’s member variable m_someInt.
  3. Initialize base class D.
  4. Initialize base class B.

Instead, we wind up with this order:

  1. Initialize base class A via its default constructor.
  2. Initialize base class B.
  3. Initialize base class C.
  4. Initialize base class D.
  5. Initialize Y’s member variable m_someInt.

Since we know B inherits virtually from A, and that B is the only source of inheritance from A, we can conclude that B is given priority over the others and that A is given priority over B.

Well, we inherit from B before we inherit from the other classes. So that could be it, but why doesn’t D become initialized right after B? It’s because D is directly inherited while C is virtually inherited. The virtuals come first.

Here are the rules:

  1. Virtual base classes are constructed in a left-to-right order as they are written in the list of base classes.
  2. If you do not call a specific constructor for a base class you have virtually inherited from, the compiler will automatically call its default constructor at the appropriate time.
  3. When determining the order of base classes to construct, base classes are initialized before their derived classes.
  4. When virtual base classes have all been constructed, then direct base classes are constructed in their left-to-right declaration order—the same as virtual base classes.
  5. When all of its base classes have been constructed, a member variable of a class is:
    • Default-initialized if there is no initializer for it.
    • Value-initialized if the initializer is an empty set of parentheses.
    • Initialized to the result of the expression within the initializer’s parentheses.
  6. Member variables are initialized in the order they are declared in the class definition.
  7. When all the initializers in a constructor have run, any code inside the constructor’s body will be executed.

When you put all of these rules together, you initially find the order B, C, D because B and C have virtual inheritance and thus come before D. Then we add A before B because B derives from A. So we end up with A, B, C, D.

Because of the rule that base classes are initialized before derived classes, and because A comes in through virtual inheritance, A is initialized with its default constructor before we even get to its initializer in the B constructor that we call. Once we do get to the B constructor, because A is already initialized, its initializer in the B constructor is simply ignored.

Class B’s member variables are initialized in the order m_a, m_b because that is the order they are declared in the class, even though in the constructor we list their initializations in the opposite order.


Delegating Constructor

Note: Visual C++ does not support delegating constructors in Visual Studio 2012 RC.

A delegating constructor calls another constructor of the same class (the target constructor). The delegating constructor can have only one initializer statement, which is the call to the target constructor. Its body can have statements; these will run after the target constructor has completely finished. Here’s an example:

If you compile and run that code using a compiler such as GCC, you will see the following output:


Copy Constructor

A copy constructor has only one mandatory parameter: a reference to a variable having the same type as the constructor’s class. A copy constructor can have other parameters as long as they are all provided with default arguments. Its purpose is to allow you to construct a copy of an object.

The compiler will provide you with a default copy constructor if it can, and it can as long as all the member variables that are classes, structures, or unions have a copy constructor. The copy constructor it provides is a shallow copy constructor. If you have a pointer to an array of data, for instance, the copy gets a copy of the pointer, not a new array containing a copy of the same data.

If you then had a delete statement in the destructor for that class, you would have one copy in an invalid state when the other was destroyed, and you’d have a runtime error when you tried to delete the memory for the second time when the remaining copy was destroyed, assuming your program had not already crashed. This is one of many reasons you should always use smart pointers. We will cover them in the chapter on RAII.

If you do not wish to use the compiler-provided copy constructor, or if the compiler cannot provide one, but you want to have one anyway, you can write a copy constructor. For example, perhaps you want a deeper copy of the data, or perhaps your class has a std::unique_ptr and you decide what an acceptable “copy” of it would be for the purposes of your class. We will see an example of this in ConstructorsSample.

A copy constructor should typically have the following declaration: SomeClass(const SomeClass&);. To avoid weird errors, the constructor should always take a const lvalue reference to the class you copy from. There’s no reason you should change the class you are copying from in a copy constructor. Making it const does no harm and provides some guarantees about your operation. A copy constructor should not be defined as explicit.


Copy Assignment Operator

If you define a custom copy constructor, you should also define a custom copy assignment operator. The result of this operator should be that the returned value is a copy of the class it is copying. This is what is invoked when you have a statement such as a = b; where a and b are both of the same type (e.g., SomeClass).

This operator is a non-static member function of its class, so it is only invoked when you are copy-assigning to an existing instance of the class. If you had something like SomeClass a = b; then it would be a copy construction, not a copy assignment.

A copy assignment operator should have the following declaration: SomeClass& operator=(const SomeClass&);.


Move Constructor

In C++, if all you had was a copy constructor, and you wanted to pass a class instance into a std::vector (similar to a .NET List<T>) or return it from a function, you would need to make a copy of it. Even if you had no intention of using it again, you would still incur the time it takes to make a copy. If you’re adding many elements to a std::vector, or if you wrote a factory function that you use a lot, it would be a big performance hit.

This is why C++ has a move constructor. The move constructor is new in C++11. There are some circumstances in which the compiler will provide you one, but generally you should write your own for the classes you will need it for.

It’s easy to do. Note that if you write a move constructor, then you will also need to write a copy constructor. Visual C++ does not enforce this rule as of Visual Studio 2012 RC, but it is part of the C++ language standard. If you need to compile your program with another compiler, you should make sure that you write copy constructors and copy assignment operators when you write a move constructor.

A move constructor should typically have the following declaration: SomeClass(SomeClass&&);. It cannot be const (because we will be modifying it) or explicit.

The std::move Function

The std::move function helps you write move constructors and move assignment operators. It’s in the <utility> header file. It takes a single argument and returns it in a condition suitable for moving. The object passed in will be returned as an rvalue reference, unless move semantics were disabled for it, in which case you will get a compiler error.


Move Assignment Operator

Whenever you write a move constructor for a class, you should also write a move assignment operator. The result of this operator should be that the returned value contains all the data of the old class. A proper move assignment operator can be called from your move constructor in order to avoid code duplication.

A move assignment operator should have the following declaration: SomeClass& operator=(SomeClass&&);.


Removing Copy or Move Semantics

If you need to remove copy or move semantics, there are two ways to do this. First, to remove copy semantics, you can declare the copy constructor and copy assignment operator as private and leave them unimplemented. C++ only cares about an implementation of a function if you attempt to use it. By doing this, any attempt to compile a program that is trying to use copy semantics will fail, producing error messages saying you are trying to use a private member of the class and there is no implementation for it (if you accidentally use it within the class itself). The same pattern of making the constructor and assignment operator private works equally well for move semantics.

The second way is new to C++11 and is currently unsupported by Visual C++ as of Visual Studio 2012 RC. With this way, you would declare the functionality as explicitly being deleted. For example, to explicitly delete a copy constructor, you would write SomeClass(const SomeClass&) = delete;. The same = delete syntax applies to assignment operators, the move constructor, and any other functionality. However, until Visual C++ supports it, you will need to stick with the first way.


Destructors and Virtual Destructors

Any class serving as the base class of another class should have a virtual destructor. You declare a virtual destructor using the virtual keyword, (e.g., : virtual ~SomeClass(void);). This way, if you cast an object down to one of its subclasses, and then subsequently destroy the object, the proper constructor will be called, ensuring that all of the resources the class had captured are freed.

Proper destructors are critical to the RAII idiom, which we will explore shortly. You should never allow an exception to be thrown in a destructor unless you catch and handle that exception within the destructor. If there is an exception you cannot handle, you should perform safe error logging and then exit from the program. You can use the std::terminate function in the <exception> header file to invoke the current terminate handler. By default, the terminate handler invokes the abort function from the <cstdlib> header. We will discuss this functionality further in our exploration of C++ Standard Library exceptions.


Operator Overloading

Operator overloading is a powerful, advanced feature of C++. You can overload operators on a per-class basis or globally with a stand-alone function. Almost every operator in C++ can be overloaded. We will see examples of overloading the copy assignment, move assignment, &, |, and |= operators shortly. For a list of other operators you can overload, and how such overloads work, I recommend visiting the MSDN documentation on the subject. It’s a useful feature, but one you may not commonly use. Looking it up when you need it is often faster than trying to memorize it right away.

Tip: Don’t overload an operator to give it a meaning that is likely to be confusing and contrary to someone’s expectations of what that overload does. For example, the + operator should generally perform an addition or concatenation operation. Making it subtract, split, divide, multiply, or anything else that would seem odd will confuse others and introduce a strong potential for bugs. This does not mean you shouldn’t put operators without clear semantic meaning in a particular situation to use. The std::wcout and std::wcin I/O functionality from the C++ Standard Library puts the >> and << operators to use in writing and reading data.

Since bit shifting would not have a particular meaning when applied to streams, repurposing these operators in this way seems odd and different, but does not lend itself to any clearly wrong conclusions about their purpose. Once you understand what the operators do when applied to streams, repurposing them adds functionality to the language that would otherwise require more code.

Sample: ConstructorsSample\Flavor.h

Sample: ConstructorsSample\Toppings.h

Sample: ConstructorsSample\IceCreamSundae.h

Sample: ConstructorsSample\IceCreamSundae.cpp

Sample: ConstructorsSample\ConstructorsSample.cpp

Conclusion

You should now have a basic understanding of constructors, destructors, and operators in C++. In the next article, we discuss RAII or Resource Acquisition Is Initialization.

This lesson represents a chapter from C++ Succinctly, a free eBook from the team at Syncfusion.
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.