Constructors are a crucial part of any C++ class. We covered the basics of constructors in § 7.1.4 (p. 262). In this section we’ll cover some additional capabilities of constructors, and deepen our coverage of the material introduced earlier.
When we define variables, we typically initialize them immediately rather than defining them and then assigning to them:
string foo = "Hello World!"; // define and initialize
string bar; // default initialized to the empty string
bar = "Hello World!"; // assign a new value to bar
Exactly the same distinction between initialization and assignment applies to the data members of objects. If we do not explicitly initialize a member in the constructor initializer list, that member is default initialized before the constructor body starts executing. For example:
// legal but sloppier way to write the Sales_data constructor: no constructor initializers
Sales_data::Sales_data(const string &s,
unsigned cnt, double price)
{
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
This version and our original definition on page 264 have the same effect: When the constructor finishes, the data members will hold the same values. The difference is that the original version initializes its data members, whereas this version assigns values to the data members. How significant this distinction is depends on the type of the data member.
We can often, but not always, ignore the distinction between whether a member is initialized or assigned. Members that are const
or references must be initialized. Similarly, members that are of a class type that does not define a default constructor also must be initialized. For example:
class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
Like any other const
object or reference, the members ci
and ri
must be initialized. As a result, omitting a constructor initializer for these members is an error:
// error: ci and ri must be initialized
ConstRef::ConstRef(int ii)
{ // assignments:
i = ii; // ok
ci = ii; // error: cannot assign to a const
ri = i; // error: ri was never initialized
}
By the time the body of the constructor begins executing, initialization is complete. Our only chance to initialize const
or reference data members is in the constructor initializer. The correct way to write this constructor is
// ok: explicitly initialize reference and const members
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }
We must use the constructor initializer list to provide values for members that are
const
, reference, or of a class type that does not have a default constructor.
In many classes, the distinction between initialization and assignment is strictly a matter of low-level efficiency: A data member is initialized and then assigned when it could have been initialized directly.
More important than the efficiency issue is the fact that some data members must be initialized. By routinely using constructor initializers, you can avoid being surprised by compile-time errors when you have a class with a member that requires a constructor initializer.
Not surprisingly, each member may be named only once in the constructor initializer. After all, what might it mean to give a member two initial values?
What may be more surprising is that the constructor initializer list specifies only the values used to initialize the members, not the order in which those initializations are performed.
Members are initialized in the order in which they appear in the class definition: The first member is initialized first, then the next, and so on. The order in which initializers appear in the constructor initializer list does not change the order of initialization.
The order of initialization often doesn’t matter. However, if one member is initialized in terms of another, then the order in which members are initialized is crucially important.
As an example, consider the following class:
class X {
int i;
int j;
public:
// undefined: i is initialized before j
X(int val): j(val), i(j) { }
};
In this case, the constructor initializer makes it appear as if j
is initialized with val
and then j
is used to initialize i
. However, i
is initialized first. The effect of this initializer is to initialize i
with the undefined value of j
!
Some compilers are kind enough to generate a warning if the data members are listed in the constructor initializer in a different order from the order in which the members are declared.
It is a good idea to write constructor initializers in the same order as the members are declared. Moreover, when possible, avoid using members to initialize other members.
If possible, it is a good idea write member initializers to use the constructor’s parameters rather than another data member from the same object. That way we don’t even have to think about the order of member initialization. For example, it would be better to write the constructor for X
as
X(int val): i(val), j(val) { }
In this version, the order in which i
and j
are initialized doesn’t matter.
The actions of the Sales_data
default constructor are similar to those of the constructor that takes a single string
argument. The only difference is that the constructor that takes a string
argument uses that argument to initialize bookNo
. The default constructor (implicitly) uses the string
default constructor to initialize bookNo
. We can rewrite these constructors as a single constructor with a default argument (§ 6.5.1, p. 236):
class Sales_data {
public:
// defines the default constructor as well as one that takes a string argument
Sales_data(std::string s = ""): bookNo(s) { }
// remaining constructors unchanged
Sales_data(std::string s, unsigned cnt, double rev):
bookNo(s), units_sold(cnt), revenue(rev*cnt) { }
Sales_data(std::istream &is) { read(is, *this); }
// remaining members as before
};
This version of our class provides the same interface as our original on page 264. Both versions create the same object when given no arguments or when given a single string
argument. Because we can call this constructor with no arguments, this constructor defines a default constructor for our class.
A constructor that supplies default arguments for all its parameters also defines the default constructor.
It is worth noting that we probably should not use default arguments with the Sales_data
constructor that takes three arguments. If a user supplies a nonzero count for the number of books sold, we want to ensure that the user also supplies the price at which those books were sold.
Exercises Section 7.5.1
Exercise 7.36: The following initializer is in error. Identify and fix the problem.
struct X {
X (int i, int j): base(i), rem(base % j) { }
int rem, base;
};Exercise 7.37: Using the version of
Sales_data
from this section, determine which constructor is used to initialize each of the following variables and list the values of the data members in each object:Sales_data first_item(cin);
int main() {
Sales_data next;
Sales_data last("9-999-99999-9");
}Exercise 7.38: We might want to supply
cin
as a default argument to the constructor that takes anistream&
. Write the constructor declaration that usescin
as a default argument.Exercise 7.39: Would it be legal for both the constructor that takes a
string
and the one that takes anistream&
to have default arguments? If not, why not?Exercise 7.40: Choose one of the following abstractions (or an abstraction of your own choosing). Determine what data are needed in the class. Provide an appropriate set of constructors. Explain your decisions.
(a)
Book
(b)
Date
(c)
Employee
(d)
Vehicle
(e)
Object
(f)
Tree
The new standard extends the use of constructor initializers to let us define so-called delegating constructors. A delegating constructor uses another constructor from its own class to perform its initialization. It is said to “delegate” some (or all) of its work to this other constructor.
Like any other constructor, a delegating constructor has a member initializer list and a function body. In a delegating constructor, the member initializer list has a single entry that is the name of the class itself. Like other member initializers, the name of the class is followed by a parenthesized list of arguments. The argument list must match another constructor in the class.
As an example, we’ll rewrite the Sales_data
class to use delegating constructors as follows:
class Sales_data {
public:
// nondelegating constructor initializes members from corresponding arguments
Sales_data(std::string s, unsigned cnt, double price):
bookNo(s), units_sold(cnt), revenue(cnt*price) { }
// remaining constructors all delegate to another constructor
Sales_data(): Sales_data("", 0, 0) {}
Sales_data(std::string s): Sales_data(s, 0,0) {}
Sales_data(std::istream &is): Sales_data()
{ read(is, *this); }
// other members as before
};
In this version of Sales_data
, all but one of the constructors delegate their work. The first constructor takes three arguments, uses those arguments to initialize the data members, and does no further work. In this version of the class, we define the default constructor to use the three-argument constructor to do its initialization. It too has no additional work, as indicated by the empty constructor body. The constructor that takes a string
also delegates to the three-argument version.
The constructor that takes an istream&
also delegates. It delegates to the default constructor, which in turn delegates to the three-argument constructor. Once those constructors complete their work, the body of the istream&
constructor is run. Its constructor body calls read
to read the given istream
.
When a constructor delegates to another constructor, the constructor initializer list and function body of the delegated-to constructor are both executed. In Sales_data
, the function bodies of the delegated-to constructors happen to be empty. Had the function bodies contained code, that code would be run before control returned to the function body of the delegating constructor.
Exercises Section 7.5.2
Exercise 7.41: Rewrite your own version of the
Sales_data
class to use delegating constructors. Add a statement to the body of each of the constructors that prints a message whenever it is executed. Write declarations to construct aSales_data
object in every way possible. Study the output until you are certain you understand the order of execution among delegating constructors.Exercise 7.42: For the class you wrote for exercise 7.40 in § 7.5.1 (p. 291), decide whether any of the constructors might use delegation. If so, write the delegating constructor(s) for your class. If not, look at the list of abstractions and choose one that you think would use a delegating constructor. Write the class definition for that abstraction.
The default constructor is used automatically whenever an object is default or value initialized. Default initialization happens
• When we define non
static
variables (§ 2.2.1, p. 43) or arrays (§3.5.1, p. 114) at block scope without initializers
• When a class that itself has members of class type uses the synthesized default constructor (§ 7.1.4, p. 262)
• When members of class type are not explicitly initialized in a constructor initializer list (§ 7.1.4, p. 265)
Value initialization happens
• During array initialization when we provide fewer initializers than the size of the array (§ 3.5.1, p. 114)
• When we define a local static object without an initializer (§ 6.1.1, p. 205)
• When we explicitly request value initialization by writing an expressions of the form
T()
whereT
is the name of a type (Thevector
constructor that takes a single argument to specify thevector
’s size (§ 3.3.1, p. 98) uses an argument of this kind to value initialize its element initializer.)
Classes must have a default constructor in order to be used in these contexts. Most of these contexts should be fairly obvious.
What may be less obvious is the impact on classes that have data members that do not have a default constructor:
class NoDefault {
public:
NoDefault(const std::string&);
// additional members follow, but no other constructors
};
struct A { // my_mem is public by default; see § 7.2 (p. 268)
NoDefault my_mem;
};
A a; // error: cannot synthesize a constructor for A
struct B {
B() {} // error: no initializer for b_member
NoDefault b_member;
};
In practice, it is almost always right to provide a default constructor if other constructors are being defined.
The following declaration of obj
compiles without complaint. However, when we try to use obj
Sales_data obj(); // ok: but defines a function, not an object
if (obj.isbn() == Primer_5th_ed.isbn()) // error: obj is a function
the compiler complains that we cannot apply member access notation to a function. The problem is that, although we intended to declare a default-initialized object, obj
actually declares a function taking no parameters and returning an object of type Sales_data
.
The correct way to define an object that uses the default constructor for initialization is to leave off the trailing, empty parentheses:
// ok: obj is a default-initialized object
Sales_data obj;
It is a common mistake among programmers new to C++ to try to declare an object initialized with the default constructor as follows:
Sales_data obj(); // oops! declares a function, not an object
Sales_data obj2; // ok: obj2 is an object, not a function
Exercises Section 7.5.3
Exercise 7.43: Assume we have a class named
NoDefault
that has a constructor that takes anint
, but has no default constructor. Define a classC
that has a member of typeNoDefault
. Define the default constructor forC
.Exercise 7.44: Is the following declaration legal? If not, why not?
vector<NoDefault> vec(10);
Exercise 7.45: What if we defined the
vector
in the previous execercise to hold objects of typeC
?Exercise 7.46: Which, if any, of the following statements are untrue? Why?
(a) A class must provide at least one constructor.
(b) A default constructor is a constructor with an empty parameter list.
(c) If there are no meaningful default values for a class, the class should not provide a default constructor.
(d) If a class does not define a default constructor, the compiler generates one that initializes each data member to the default value of its associated type.
As we saw in § 4.11 (p. 159), the language defines several automatic conversions among the built-in types. We also noted that classes can define implicit conversions as well. Every constructor that can be called with a single argument defines an implicit conversion to a class type. Such constructors are sometimes referred to as converting constructors. We’ll see in § 14.9 (p. 579) how to define conversions from a class type to another type.
A constructor that can be called with a single argument defines an implicit conversion from the constructor’s parameter type to the class type.
The Sales_data
constructors that take a string
and that take an istream
both define implicit conversions from those types to Sales_data
. That is, we can use a string
or an istream
where an object of type Sales_data
is expected:
string null_book = "9-999-99999-9";
// constructs a temporary Sales_data object
// with units_sold and revenue equal to 0 and bookNo equal to null_book
item.combine(null_book);
Here we call the Sales_data combine
member function with a string
argument. This call is perfectly legal; the compiler automatically creates a Sales_data
object from the given string
. That newly generated (temporary) Sales_data
is passed to combine
. Because combine
’s parameter is a reference to const
, we can pass a temporary to that parameter.
In § 4.11.2 (p. 162) we noted that the compiler will automatically apply only one class-type conversion. For example, the following code is in error because it implicitly uses two conversions:
// error: requires two user-defined conversions:
// (1) convert "9-999-99999-9" to string
// (2) convert that (temporary) string to Sales_data
item.combine("9-999-99999-9");
If we wanted to make this call, we can do so by explicitly converting the character string to either a string
or a Sales_data
object:
// ok: explicit conversion to string, implicit conversion to Sales_data
item.combine(string("9-999-99999-9"));
// ok: implicit conversion to string, explicit conversion to Sales_data
item.combine(Sales_data("9-999-99999-9"));
Whether the conversion of a string
to Sales_data
is desired depends on how we think our users will use the conversion. In this case, it might be okay. The string
in null_book
probably represents a nonexistent ISBN.
More problematic is the conversion from istream
to Sales_data
:
// uses the istream constructor to build an object to pass to combine
item.combine(cin);
This code implicitly converts cin
to Sales_data
. This conversion executes the Sales_data
constructor that takes an istream
. That constructor creates a (temporary) Sales_data
object by reading the standard input. That object is then passed to combine
.
This Sales_data
object is a temporary (§ 2.4.1, p. 62). We have no access to it once combine
finishes. Effectively, we have constructed an object that is discarded after we add its value into item
.
We can prevent the use of a constructor in a context that requires an implicit conversion by declaring the constructor as explicit
:
class Sales_data {
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) { }
explicit Sales_data(const std::string &s): bookNo(s) { }
explicit Sales_data(std::istream&);
// remaining members as before
};
Now, neither constructor can be used to implicitly create a Sales_data
object. Neither of our previous uses will compile:
item.combine(null_book); // error: string constructor is explicit
item.combine(cin); // error: istream constructor is explicit
The explicit
keyword is meaningful only on constructors that can be called with a single argument. Constructors that require more arguments are not used to perform an implicit conversion, so there is no need to designate such constructors as explicit
. The explicit
keyword is used only on the constructor declaration inside the class. It is not repeated on a definition made outside the class body:
// error: explicit allowed only on a constructor declaration in a class header
explicit Sales_data::Sales_data(istream& is)
{
read(is, *this);
}
explicit
Constructors Can Be Used Only for Direct InitializationOne context in which implicit conversions happen is when we use the copy form of initialization (with an =
) (§ 3.2.1, p. 84). We cannot use an explicit
constructor with this form of initialization; we must use direct initialization:
Sales_data item1 (null_book); // ok: direct initialization
// error: cannot use the copy form of initialization with an explicit constructor
Sales_data item2 = null_book;
When a constructor is declared
explicit
, it can be used only with the direct form of initialization (§ 3.2.1, p. 84). Moroever, the compiler will not use this constructor in an automatic conversion.
Although the compiler will not use an explicit
constructor for an implicit conversion, we can use such constructors explicitly to force a conversion:
// ok: the argument is an explicitly constructed Sales_data object
item.combine(Sales_data(null_book));
// ok: static_cast can use an explicit constructor
item.combine(static_cast<Sales_data>(cin));
In the first call, we use the Sales_data
constructor directly. This call constructs a temporary Sales_data
object using the Sales_data
constructor that takes a string
. In the second call, we use a static_cast
(§ 4.11.3, p. 163) to perform an explicit, rather than an implicit, conversion. In this call, the static_cast
uses the istream
constructor to construct a temporary Sales_data
object.
explicit
ConstructorsSome of the library classes that we’ve used have single-parameter constructors:
• The
string
constructor that takes a single parameter of typeconst char*
(§ 3.2.1, p. 84) is notexplicit
.
• The
vector
constructor that takes a size (§ 3.3.1, p. 98) isexplicit
.
Exercises Section 7.5.4
Exercise 7.47: Explain whether the
Sales_data
constructor that takes astring
should beexplicit
. What are the benefits of making the constructorexplicit
? What are the drawbacks?Exercise 7.48: Assuming the
Sales_data
constructors are notexplicit
, what operations happen during the following definitionsstring null_isbn("9-999-99999-9");
Sales_data item1(null_isbn);
Sales_data item2("9-999-99999-9");What happens if the
Sales_data
constructors areexplicit
?Exercise 7.49: For each of the three following declarations of
combine
, explain what happens if we calli.combine(s)
, wherei
is aSales_data
ands
is astring
:(a)
Sales_data &combine(Sales_data);
(b)
Sales_data &combine(Sales_data&);
(c)
Sales_data &combine(const Sales_data&) const;
Exercise 7.50: Determine whether any of your
Person
class constructors should beexplicit
.Exercise 7.51: Why do you think
vector
defines its single-argument constructor asexplicit
, butstring
does not?
An aggregate class gives users direct access to its members and has special initialization syntax. A class is an aggregate if
• All of its data members are
public
• It does not define any constructors
• It has no base classes or
virtual
functions, which are class-related features that we’ll cover in Chapter 15
For example, the following class is an aggregate:
struct Data {
int ival;
string s;
};
We can initialize the data members of an aggregate class by providing a braced list of member initializers:
// val1.ival = 0; val1.s = string("Anna")
Data val1 = { 0, "Anna" };
The initializers must appear in declaration order of the data members. That is, the initializer for the first member is first, for the second is next, and so on. The following, for example, is an error:
// error: can't use "Anna" to initialize ival, or 1024 to initialize s
Data val2 = { "Anna", 1024 };
As with initialization of array elements (§ 3.5.1, p. 114), if the list of initializers has fewer elements than the class has members, the trailing members are value initialized (§ 3.5.1, p. 114). The list of initializers must not contain more elements than the class has members.
It is worth noting that there are three significant drawbacks to explicitly initializing the members of an object of class type:
• It requires that all the data members of the class be
public
.
• It puts the burden on the user of the class (rather than on the class author) to correctly initialize every member of every object. Such initialization is tedious and error-prone because it is easy to forget an initializer or to supply an inappropriate initializer.
• If a member is added or removed, all initializations have to be updated.
Exercises Section 7.5.5
Exercise 7.52: Using our first version of
Sales_data
from § 2.6.1 (p. 72), explain the following initialization. Identify and fix any problems.Sales_data item = {"978-0590353403", 25, 15.99};
In § 6.5.2 (p. 239) we noted that the parameters and return type of a constexpr
function must be literal types. In addition to the arithmetic types, references, and pointers, certain classes are also literal types. Unlike other classes, classes that are literal types may have function members that are constexpr
. Such members must meet all the requirements of a constexpr
function. These member functions are implicitly const
(§ 7.1.2, p. 258).
An aggregate class (§ 7.5.5, p. 298) whose data members are all of literal type is a literal class. A nonaggregate class, that meets the following restrictions, is also a literal class:
• The data members all must have literal type.
• The class must have at least one
constexpr
constructor.
• If a data member has an in-class initializer, the initializer for a member of built-in type must be a constant expression (§ 2.4.4, p. 65), or if the member has class type, the initializer must use the member’s own
constexpr
constructor.
• The class must use default definition for its destructor, which is the member that destroys objects of the class type (§ 7.1.5, p. 267).
constexpr
ConstructorsAlthough constructors can’t be const
(§ 7.1.4, p. 262), constructors in a literal class can be constexpr
(§ 6.5.2, p. 239) functions. Indeed, a literal class must provide at least one constexpr
constructor.
A constexpr
constructor can be declared as = default
(§ 7.1.4, p. 264) (or as a deleted function, which we cover in § 13.1.6 (p. 507)). Otherwise, a constexpr
constructor must meet the requirements of a constructor—meaning it can have no return
statement—and of a constexpr
function—meaning the only executable statement it can have is a return
statement (§ 6.5.2, p. 239). As a result, the body of a constexpr
constructor is typically empty. We define a constexpr
constructor by preceding its declaration with the keyword constexpr
:
class Debug {
public:
constexpr Debug(bool b = true): hw(b), io(b), other(b) { }
constexpr Debug(bool h, bool i, bool o):
hw(h), io(i), other(o) { }
constexpr bool any() { return hw || io || other; }
void set_io(bool b) { io = b; }
void set_hw(bool b) { hw = b; }
void set_other(bool b) { hw = b; }
private:
bool hw; // hardware errors other than IO errors
bool io; // IO errors
bool other; // other errors
};
A constexpr
constructor must initialize every data member. The initializers must either use a constexpr
constructor or be a constant expression.
A constexpr
constructor is used to generate objects that are constexpr
and for parameters or return types in constexpr
functions:
constexpr Debug io_sub(false, true, false); // debugging IO
if (io_sub.any()) // equivalent to if(true)
cerr << "print appropriate error messages" << endl;
constexpr Debug prod(false); // no debugging during production
if (prod.any()) // equivalent to if(false)
cerr << "print an error message" << endl;
Exercises Section 7.5.6
Exercise 7.54: Should the members of
Debug
that begin withset_
be declared asconstexpr
? If not, why not?Exercise 7.55: Is the
Data
class from § 7.5.5 (p. 298) a literal class? If not, why not? If so, explain why it is literal.