char
to int
and from short
to double
. This is why you can pass a short
to a function that expects a double
and still have the call succeed. The more frightening conversions in C -- those that may lose information -- are also present in C++, including conversion of int
to short
and double
to (of all things) char
.You can't do anything about such conversions, because they're hard-coded into the language. When you add your own types, however, you have more control, because you can choose whether to provide the functions compilers are allowed to use for implicit type conversions.
Two kinds of functions allow compilers to perform such conversions: single-argument constructors and implicit type conversion operators. A single-argument constructor is a constructor that may be called with only one argument. Such a constructor may declare a single parameter or it may declare multiple parameters, with each parameter after the first having a default value. Here are two examples:
class Name { // for names of thingsAn implicit type conversion operator is simply a member function with a strange-looking name: the word
public:
Name(const string& s); // converts string to
// Name
... };class Rational { // for rational numbers
public:
Rational(int numerator = 0, // converts int to
int denominator = 1); // Rational
... };
operator
followed by a type specification. You aren't allowed to specify a type for the function's return value, because the type of the return value is basically just the name of the function. For example, to allow Rational
objects to be implicitly converted to double
s (which might be useful for mixed-mode arithmetic involving Rational
objects), you might define class Rational
like this:
class Rational {This function would be automatically invoked in contexts like this:
public:
...
operator double() const; // converts Rational to
}; // double
Rational r(1, 2); // r has the value 1/2Perhaps all this is review. That's fine, because what I really want to explain is why you usually don't want to provide type conversion functions of any ilk.double d = 0.5 * r; // converts r to a double,
// then does multiplication
The fundamental problem is that such functions often end up being called when you neither want nor expect them to be. The result can be incorrect and unintuitive program behavior that is maddeningly difficult to diagnose.
Let us deal first with implicit type conversion operators, as they are the easiest case to handle. Suppose you have a class for rational numbers similar to the one above, and you'd like to print Rational
objects as if they were a built-in type. That is, you'd like to be able to do this:
Rational r(1, 2);Further suppose you forgot to write ancout << r; // should print "1/2"
operator<<
for Rational
objects. You would probably expect that the attempt to print r would fail, because there is no appropriate operator<<
to call. You would be mistaken. Your compilers, faced with a call to a function called operator<<
that takes a Rational
, would find that no such function existed, but they would then try to find an acceptable sequence of implicit type conversions they could apply to make the call succeed. The rules defining which sequences of conversions are acceptable are complicated, but in this case your compilers would discover they could make the call succeed by implicitly converting r
to a double
by calling Rational::operator double
. The result of the code above would be to print r
as a floating point number, not as a rational number. This is hardly a disaster, but it demonstrates the disadvantage of implicit type conversion operators: their presence can lead to the wrong function being called (i.e., one other than the one intended). The solution is to replace the operators with equivalent functions that don't have the syntactically magic names. For example, to allow conversion of a Rational
object to a double
, replace operator double
with a function called something like asDouble
:
class Rational {Such a member function must be called explicitly:
public:
...
double asDouble() const; // converts Rational
}; // to double
Rational r(1, 2);In most cases, the inconvenience of having to call conversion functions explicitly is more than compensated for by the fact that unintended functions can no longer be silently invoked. In general, the more experience C++ programmers have, the more likely they are to eschew type conversion operators. The members of the committee working on the standard C++ library (see Item 35), for example, are among the most experienced in the business, and perhaps that's why thecout << r; // error! No operator<<
// for Rationalscout << r.asDouble(); // fine, prints r as a
// double
string
class they added to the library contains no implicit conversion from a string
object to a C-style char*
. Instead, there's an explicit member function, c_str
, that performs that conversion. Coincidence? I think not.Implicit conversions via single-argument constructors are more difficult to eliminate. Furthermore, the problems these functions cause are in many cases worse than those arising from implicit type conversion operators.
As an example, consider a class template for array objects. These arrays allow clients to specify upper and lower index bounds:
template<class T>The first constructor in the class allows clients to specify a range of array indices, for example, from 10 to 20. As a two-argument constructor, this function is ineligible for use as a type-conversion function. The second constructor, which allows clients to define
class Array {
public:
Array(int lowBound, int highBound);
Array(int size);T& operator[](int index);
...
};
Array
objects by specifying only the number of elements in the array (in a manner similar to that used with built-in arrays), is different. It can be used as a type conversion function, and that can lead to endless anguish. For example, consider a template specialization for comparing Array<int>
objects and some code that uses such objects:
bool operator==(const Array<int>& lhs,We intended to compare each element of
const Array<int>& rhs);Array<int> a(10);
Array<int> b(10);...
for (int i = 0; i < 10; ++i)
if (a == b[i]) { // oops! "a" should be "a[i]"
do something for when
a[i] and b[i] are equal;
}
else {
do something for when they're not;
}
a
to the corresponding element in b
, but we accidentally omitted the subscripting syntax when we typed a