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 type const char*
(§ 3.2.1, p. 84) is not explicit
.
• The vector
constructor that takes a size (§ 3.3.1, p. 98) is explicit
.
Exercise 7.47: Explain whether the Sales_data
constructor that takes a string
should be explicit
. What are the benefits of making the constructor explicit
? What are the drawbacks?
Exercise 7.48: Assuming the Sales_data
constructors are not explicit
, what operations happen during the following definitions
string 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 are explicit
?
Exercise 7.49: For each of the three following declarations of combine
, explain what happens if we call i.combine(s)
, where i
is a Sales_data
and s
is a string
:
(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 be explicit
.
Exercise 7.51: Why do you think vector
defines its single-argument constructor as explicit
, but string
does not?