Ch13.6: Constructors: How an Object Comes Into Existence

中文(简体)

1. The Job of a Constructor

In Chapter 13.1 you could write point a{3, 4}; because the members were public and the class had no constructor — the braces filled the members in order. That syntax is a hole in the wall, and it disappears the moment the data is private. Something has to take the two numbers from the caller and put them where they belong, and that something is a constructor.

A constructor:


class point
{
public:
	point(::std::int_least32_t x, ::std::int_least32_t y) noexcept : x_{x}, y_{y} {}
	//                                                              ^^^^^^^^
	//                                        the member initializer list

private:
	::std::int_least32_t x_{};
	::std::int_least32_t y_{};
};

point a{3, 4};   // still braces, but now they call the constructor
The member initializer list is the only place data members are initialized. The body of the constructor runs after every member already has a value. Assigning in the body (x_ = x;) is a second write, and for members that cannot be assigned at all it is not an option. Initialize in the list, then do the rest in the body.

2. Members Are Initialized in Declaration Order

The list is written by you, but the compiler reads it in declaration order of the members, not left to right. If the two orders disagree, the write order in the list is a lie that readers will believe, and the compiler warns about it:


class box
{
public:
	box(::std::int_least32_t w, ::std::int_least32_t h) noexcept : h_{h}, w_{w} {}
private:
	::std::int_least32_t w_{};   // w_ is initialized first...
	::std::int_least32_t h_{};   // ...then h_, whatever the list says
};

warning: 'box::h_' will be initialized after [-Wreorder]
warning:   'int_least32_t box::w_' [-Wreorder]
warning:   when initialized here [-Wreorder]

The fix is free: write the list in the same order as the declarations, so the two never disagree. It matters as soon as one member is initialized from another — a member used before it is initialized is a bug that only shows up in some builds.

3. Everything at Once


#include <fast_io.h>
#include <cstdint>
#include <cstddef>
using namespace ::fast_io::io;

class point
{
public:
	point() noexcept : point{0, 0} // delegating constructor
	{
	}
	explicit point(::std::int_least32_t both) noexcept : point{both, both}
	{
	}
	point(::std::int_least32_t x, ::std::int_least32_t y) noexcept : x_{x}, y_{y}
	{
	}

	::std::int_least32_t x() const noexcept
	{
		return x_;
	}
	::std::int_least32_t y() const noexcept
	{
		return y_;
	}

private:
	::std::int_least32_t x_{};
	::std::int_least32_t y_{};
};

class label
{
public:
	// In-class initializers: what a member is worth when no constructor mentions it.
	::std::size_t length{};
	char first{'-'};
};

void draw(point const &p) throws
{
	println("point(", p.x(), ", ", p.y(), ")");
}

int main()
{
	try
	{
		point origin{};
		draw(origin);

		point square{3}; // explicit point(int_least32_t)
		draw(square);

		point p{7, 9};
		draw(p);

		label tag{};
		println("length = ", tag.length, ", first = ", ::fast_io::mnp::chvw(tag.first));
	}
	catch throws (::std::error e)
	{
		print("failed: ", e, "\n");
	}
}

point(0, 0)
point(3, 3)
point(7, 9)
length = 0, first = -

The four ideas in that program:

4. explicit: Refusing the Quiet Conversion

A constructor that takes one argument is also a conversion from that argument to the class type. That is what lets this compile:


void draw(point const &p) throws;

draw(5);   // silently means: point{5}

When a single number is a complete description of a point (a square), that is pleasant. Most of the time it is a trap: nobody reading draw(5) knows that a point was constructed behind their back, and the conversion will happen in the middle of overload resolution too. explicit on the constructor forbids exactly that one conversion, and nothing else:


explicit point(::std::int_least32_t both) noexcept : point{both, both} {}

error: invalid initialization of reference of type 'const point&' from expression
       of type 'int'
note:   in passing argument 1 of 'void draw(const point&)' 

point{3} and point square{3}; still work — the conversion through braces is deliberate there. What stops working is the invisible one. Mark every single-argument constructor explicit unless you actually want the class to behave like the argument; that is the default in this tutorial.

Constructors that can fail are throws. Validating arguments is normal (account in Ch13.5 refuses a negative opening balance). A validating constructor is declared throws, throws ::std::errc::invalid_argument, and an object that exists is then guaranteed to be valid — for the rest of the program, with no further checks.