05 - explicit and std::initializer_list

Now you should understand that constructors offer the ability to control object initialization. There are 2 further minor features.

Convering constructors

Since the keyword explicit wasn't used in any prior example, all constructors written so far were converting constructors.

What does it mean? It means that the call to the object constructor is allowed to perform implicit convertions if necesary. A good example is std::string which has a non-explicit constructor overload accepting pointers to character arrays. This allows to do such things:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string>

void print(std::string str)
{
	std::cout << str;
}

int main()
{
	print("Hello\n");
	return 0;
}

The function accepts an object of type std::string, however the argument is an array of characters (const char[7] to be specific - 5 characters, 1 escaped character and null terminator). Because std::string has a non-explicit constructor overload that accepts const char*, there is no "function parameter/argument type mismatch" error. The compiler notices that the provided argument is not of type std::string but it also notices that it can be constructed from the provided argument.

Reminder: function arguments passed by value always undergo a set of implicit convertions (decay) where most notably top-level const is stripped and raw arrays (T[N]) lose size information and become raw pointers (T*).

Adding explicit to constructors simply disallows implicit convertions (other than decay) and some forms of initialization:

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
struct point
{
	point() = default;
	explicit point(int val)
	: x(val), y(val) {}
	explicit point(int x, int y)
	: x(x), y(y) {}

	int x = 0;
	int y = 0;
};

point make_point(int x, int y)
{
	// return {x, y};   // error: constructor is explicit
	return point{x, y}; // ok
}

int main()
{
	// point p1 = 1;      // error: copy-initialization is not allowed
	point p2(2);          // ok: direct-initialization
	point p3{3, 3};       // ok: direct-list-initialization
	// point p4 = {4, 4}; // error: copy-list-initialization is not allowed
}

This leads to the question - when should a constructor be marked explicit?

This is because implicit convertions are generally undesirable. For few cases where the convertion is desired (such as fraction fr = 1;) some people write /* implicit */ before the constructor to signify it's intentionally not marked explicit.

std::initializer_list

Not to be confused with member initialization syntax, this simple class is intended to hold arbitrary amount of arguments of specific type for the purpose of initialization. The class is implemented as lightweight proxy object. Copying std::initializer_list does not copy underlying objects and objects from the list are read-only.

Because std::initializer_list is intended for construction of other objects, it has special rules for its own construction:

  • It is automatically constructed when constructors accept it as a parameter and the syntax used to initialize is {} or =.

  • It is automatically constructed from braced init lists that are bound to auto.

1
2
3
4
5
6
7
8
// required even if the name is not explicitly used
#include <initializer_list>

auto f()
{
	auto list = {1, 2, 3}; // auto = std::initializer_list<int>
	return list; // return {1, 2, 3}; would require to specify function return type
}

This can be particulary surprising when a type has multiple overloads, some of which use this special class:

1
2
3
4
// overload (size_type count, const T& value): {2, 2, 2, 2, 2}
std::vector<int> v1(5, 2);
// overload (std::initializer_list<T> init): {5, 2}
std::vector<int> v2{5, 2};

In such case my recommendation is to write v2 = {5, 2} which is much clearer about intent.

How about combining explicit with std::initializer_list?

Don't. The whole point of std::initializer_list is to offer syntax sugar for construction, without having to explicitly write the class name.