04 - constructors
Remember recent problem of being unable to create objects with custom initial values? This lesson will solve it.
Constructors are one of special member functions. They are automatically called whenever an object of their type is created. They have 2 main purposes:
initialize all fields
establish class invariants
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <iostream> int make_valid_denominator(int value) { if (value == 0) return 1; else return value; } class fraction { private: int numerator = 0; int denominator = 1; public: // constructor fraction(int numerator, int denominator) { set(numerator, denominator); } void set(int count, int denom) { numerator = count; denominator = make_valid_denominator(denom); } }; int main() { // this is how you use a constructor to create an object fraction fr1(1, 2); // some people prefer to do this because it looks like a function call // the ctor creates a temporary object, then another object is assigned from it // since C++17 this is guaranteed to be optimized to avoid temporary and assignment auto fr2 = fraction(2, 3); } |
Pay attention to the syntax:
Constructors always use the name of the class.
Constructors do not have a return type - not even
void
.
Besides these, constructors behave just like member functions:
Constructors are affected by access specifiers.
It's possible to overload constructors (including
= delete
). If no overload can be selected - compilation error.It's possible to place a
return;
statement in their body, just like in a function returningvoid
.
Initialization vs assignment
As you already know, when creating objects, initialization should always be preferred over assignment. Creating uninitialized objects only asks for trouble (and if the initial value cannot be given it means the object has too large lifetime and is not needed that early).
Constructors use special syntax for initialization - member initializer list:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class fraction { public: // [...] // BAD: fields are assigned // If field definitions have no default initializers, // they are uninitialized until assigned! fraction(int num, int denom) { // both statements are assignments, not initialization numerator = num; denominator = make_valid_denominator(denom); } // GOOD: fields are initialized // the syntax is : followed by comma-separated list of members // members can be initialized using () and {} // the formatting below aligns : and , fraction(int numerator, int denominator) : numerator(numerator) , denominator(make_valid_denominator(denominator)) {} // [...] }; |
Member intializer list is placed before the body, it starts with :
and member initializers are separated by ,
. The example presents a common style of formatting.
You might have also noticed a surprising thing - parameter names are identical to member names. This is a special feature of member intializer list - names in the list (outside parentheses and constructor body) will refer to class members.
The list does not have to contain all class members - if any of them already have an initializer defined in the class body, they can be skipped. If a member is present in the list, its initialization code simply overrides the default initializer.
Initialization order
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class point { private: int x; int y; int z; public: point(int x, int y, int z) : x(x), y(y), z(z) {} // ok point(int value) // x is initialized first, using uninitialized z // y is initialized second, using uninitialized z // z is initialized third, using parameter value // reading uninitialized z => undefined behavior! : z(value), x(z), y(z) {} }; |
main.cpp: In constructor ‘point::point(int)’: main.cpp:6:6: warning: ‘point::z’ will be initialized after [-Wreorder] int z; ^ main.cpp:4:6: warning: ‘int point::x’ [-Wreorder] int x; ^ main.cpp:12:2: warning: when initialized here [-Wreorder] point(int value) ^~~~~
Order of fields in member initializer list has no influence on the initialization order - it's defined by the order of fields in the class definition. This leads to a simple conclusion:
Delegating constructors
The fraction
class can offer 3 reasonable possibilities of initialization:
0 arguments: the object should represent zero (0/1)
1 argument: the object should represent a whole number (x/1)
2 arguments: the object will have both values specified explicitly (x/y)
The are multiple ways to achieve it. There is nothing special in overloading constructors, but there is an additional feature: constructor delegation. A constructor can replace member initializer list with a call to a different overload.
Below I showcase various ways to achieve support for 0, 1 and 2 arguments:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// A: 3 overloads with member init list fraction() : numerator(0) , denominator(1) {} fraction(int numerator) : numerator(numerator) , denominator(1) {} fraction(int numerator, int denominator) : numerator(numerator) , denominator(make_valid_denominator(denominator)) {} // B: 3 overloads, 2 overloads use delegation fraction() : fraction(0, 1) {} fraction(int numerator) : fraction(numerator, 1) {} fraction(int numerator, int denominator) : numerator(numerator) , denominator(make_valid_denominator(denominator)) {} // C: 1 overload using default parameters fraction(int numerator = 0, int denominator = 1) : numerator(numerator) , denominator(make_valid_denominator(denominator)) {} |
All of A, B and C are valid. The goal is to write least amount of code and avoid code duplication. In the case of fraction
class, C wins because only 1 function has to be written.
Your preference should be in this order:
default arguments (each default argument acts as another overload)
delegation
separate overloads
Constructor delegation helps avoiding writing member initializer list multiple times.
Calling constructors
Calling constructors works the same way as function overloading but there is a small syntax trap when you want to call an overload with 0 parameters:
1 2 3 4 |
fraction fr1(0, 1); // 2-argument constructor fraction fr2(0); // 1-argument constructor fraction fr3(); // function declaration! fraction fr4; // 0-argument constructor |
This syntax problem is not present when you do auto fr = fraction();
.
There are 2 other ways to call constructors:
1 2 3 4 5 |
fraction fr8 = 2; // 1-argument constructor fraction fr5{0, 1}; // 2-argument constructor fraction fr6{0}; // 1-argument constructor fraction fr7{}; // 0-argument constructor (no problems) |
Reminder: =
used during object definition acts as initialization, not assignment.
=
is very convenient when you want to call a constructor with exactly 1 argument (the specific constructor overload can take more arguments as long as they have default values). For the fraction
class this allows very intuitive statements like fraction fr = 5;
(here creating a fraction 5/1). This form is not allowed if the constructor is explicit
.
{}
places additional requirement: no narrowing convertions. So giving a long
would not work because convertion from long
to int
is considered narrowing.
Default constructor
If a class does not have any constructors specified, it automatically gets a default constructor. It has the following properties:
it's
public
it takes 0 arguments
it uses initializers defined inside class body (if present)
it has empty body
If you have defined custom constructors and still want to have the default one, you can write class_name() = default;
to force its existence. You can also write class_name() = delete;
to explicitly disable its existence.
When an object of specific class can be constructed with 0 arguments, the class is default constructible. Default constructible types are easier to work with and some templates within standard and external libraries require types to satisfy this requirement. Note that this can be achieved even if the constructor takes multiple parameters - just provide default arguments (see variant C in delegation example).
Questions
Constructors are affected by access specifiers. Is there any point of other specifier than
public
for a constructor? Wouldn't other access prevent from creating an object?
It would, but only outside the class code. There are some situations where having non-public constructor is beneficial. For example, a class can have 1 private
constructor overload and many public
overloads, all which delegate to the private
one. Another case (from a later lesson) is a set of public
and static
functions that call private
constructor.
How does function overloading interact with access specifiers?
Access specifiers do not affect overload resolution - functions are selected without checking their access. If a function has multiple overloads and they differ by access level, it's possible to end up in compilation error because selected overload is not public
.
What happens if there is a loop within constructor delegations?
Same thing as with mutually recursive functions - the program will be stuck in an endless call cycle (or something worse because technically endless recursion is undefined behavior).