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:
- has the same name as the class, and no return type;
- runs automatically when an object is created — you never call it by name;
- gets the object into a state where every member function may be called — that is, into a state where the invariant holds (Ch13.5).
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
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:
-
In-class initializers (
::std::size_t length{};). A member declared with an initializer has that value whenever a constructor does not mention it. This is whylabel tag{};produceslength = 0, first = -without any constructor being written at all. Prefer them: they keep the default next to the member instead of repeating it in every constructor. -
Delegating constructors
(
point() noexcept : point{0, 0} {}). One constructor can hand its work to another of the same class. The default constructor then contains no data at all: “an origin is a point at zero, zero” is written once, in the constructor that knows how to build a point. -
Overloaded constructors are chosen by the arguments exactly
like any other overload.
point{},point{3}andpoint{7, 9}reach three different constructors. -
A default constructor is not created for you once you write
any constructor.
class pointwith onlypoint(x, y)cannot be default-constructed, and the compiler says so plainly:
If you want both, declare both.error: no matching function for call to 'point::point()'point() noexcept = default;asks the compiler to write the default constructor, which will use the in-class initializers of the members.
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.
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.