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 returning void.

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).