Oveloaded conversion operators are powerful, but potentially hazardous. Pete shows how to incorporate them as features rather than bugs.
Q
I am trying to understand how overloaded conversion operators work. I traced through a sample program below using Borland Turbo C++ 4.5 for Windows to see what's going on. At the last statement given in main, f = float(r1);, the program called the double overloaded conversion operator. There is no explicit float overloaded operator defined for the Rational class.
1. Why isn't an explicit float overload operator needed?
2. Why is the double overload operator being called for the float conversion in the first place?
3. Leaving it as it is, isn't the float(r1) statement converting r1 to double, then to float, i.e., a narrowing conversion?
Thanks.
class Rational { private: // defines a rational number as numerator/denominator long num, den; public: Rational(int num=0, int denom=1); // conversion operator: Rational to double operator double(void) const; }; int main(void) { Rational r1(5); float f; f = float(r1); }A
I've simplified your code by removing most of the definition of the Rational class in order to fit it better in this column. Answering the last question first, yes, the finalstatementusesoperatordouble() to convert r1to a double, then narrows theresult toafloat. Answering the second question second, the double operator is being called because itis the only one that can beused to convert r1 to a float. Answeringthe first question last, thereason an explicit operator float() is not required is that there is an operatordouble() that canbe used. Anythingelse?
What, that's not perfectly clear? Well, you're not alone. The interaction of built-in conversions and user-defined conversions confuses many people when they first encounter it in C++. Let'sbegin a bit more simply with the standard conversions that C++inherited from C.
Be careful ofyour terminology when you talk about conversions.This is an areawhere people tendto use words carelessly, and thatleads to considerable confusion. In particular, distinguish betweenaconversion and a cast. They are two completely different things. Aconversion is a change in representation of some value. So,forexample, when we assign the value 1 to a double, the integralvalue1 is converted to a double value. That can be done at compile timeif the compiler knows the exact value, or it can be done at run time.For example:
void f( int i ) { double d1 = 1; // conversion 1 double d2 = i; // conversion 2 }
The first conversion can be done at compile time, because the compiler can determine the exact value that is being converted. It is not required to do so, however: a conforming compiler actually can generate code to do this conversion at run time. The second conversion cannot be done at compile time, because the value to be converted is not known.Conversions can be explicit or implicit. Explicit conversions are marked in your source code by casts. Implicit conversions are not. It's that simple.
void h( double ); void g( int i ) { // explicit conversion double d1 = (double)i; // implicit conversion double d2 = i; // implicit conversion h(i); // explicit conversion h((double)i); }
C++ complicates this a bit, because objects can be converted to other types, including built-in types. For example, your class Rational provides a conversion operator that can convert an object of type Rational to a double. This conversion can be invoked in the same way as any of the built-in conversions:void h( double ); void g( Rational r ) { // explicit conversion double d1 = (double)r; // implicit conversion double d2 = r; // implicit conversion h(r); // explicit conversion h((double)r); }
C++ also provides a couple of other ways to indicate explicit conversion, through the function-style cast and the new-style casts. I won't go into those here.Your class Rational also provides a conversion operator that can convert an object of type int to an object of type Rational. You may not have thought of it that way, but that's what your constructor does: it can be called with a single argument of type int, and it produces an object of type Rational. This operation can be invoked in the same ways as any other conversion:
void h( Rational ); void g( int i ) { Rational r1 = (Rational)i; // explicit conversion Rational r2 = i; // implicit conversion h(i); // implicit conversion h((Rational)i); // explicit conversion }
There's a bit of a problem with allowing constructors to automatically serve as conversion operators. Sometimes you need to create objects by giving a single argument to the constructor, but you don't want to have things automatically converted. For example, you might want to write a string class with a constructor taking a single argument that gives the anticipated size of the string. That would allow you to write code something like this:String s(30); // eventually grow to 30 characters for( int i = 0; i < 10; i++ ) { s.insert(i+'0'); s.insert(i+'0'); s.insert(i+'0'); }
This would build a string containing "000111222333444555666777888999", without having to reallocate memory for the string's internal storage. Let's flesh out this String class a bit more, and look at some of the problems you'd run into:class String { public: String(); String( char ch ); String( const String& ); String( unsigned ); String operator += ( String ); }; int main() { int i = 0; String s; s += (i+'0'); }
That final bit of code isn't much different from the previous code sample, but it won't compile. BC++ 5.0 reports "illegal structure operation" at the last line. The problem is that the expression i+'0' is of type int. To apply the += operator, the compiler needs to convert this value into a String, and it can choose either the constructor that takes an unsigned or the one that takes a char. That's ambiguous, so the code is not valid.Now, you almost certainly did not intend for the constructor that takes an unsigned to be used in this sort of context. It rarely makes sense to create a temporary String object with no contents. So the ANSI/ISO working paper now allows you to tell the compiler not to use particular one-argument constructors when it needs to perform an implicit conversion. You do this by putting the keyword explicit in front of the constructor declaration:
class String { public: String(); String( char ch ); String( const String& ); explicit String( unsigned ); String operator += ( String ); };Now the code snippet in the previous example is valid.
I need to provide one more bit of background to show why your original code is valid. In C it was easy: it provided a set of well-defined built-in conversions, and that was that. In C++ we have added conversion operators, and single-argument constructors that can serve as conversion operators, known collectively as "user-defined conversions." User-defined conversions open up a large range of possibilities, some of which are frightening to contemplate [1] . For example:
class First { public: First(int); }; class Second { public: Second( First ); }; class Third { public: Third( Second ); }; int main() { Third th( 1 ); }There's no inherent reason that the language definition couldn't require compilers to figure this out, and construct a temporary object of type First with the parameter 1, followed by a temporary object of type Second using the temporary object of type First, followed by creating the desired object from the second temporary object. This would be quite hard to read, though, and tracing through large numbers of possible constructors to figure out what your program was actually doing would drive you nuts. So the rule is that the compiler will only use one user-defined conversion in an implicit conversion. The definition of the above requires two user-defined conversions to create an object of type Second to pass to the constructor for Third: one from int to First, and one from First to Second. Since it requires two user-defined conversion, it is not valid.
However, this restriction does not apply to built-in conversions. The thinking is that the set of built-in conversions is well-defined by the language itself, so users of the language won't have to hunt around for possible conversions outside of the ones they have written themselves. The compiler can use built-in conversions in the course of converting one type to another. That's why the final line in your original code is valid: to convert an object of type Rational to a float, the compiler first calls the user-defined conversion from Rational to double, then uses the built-in conversion from double to float. Simple, isn't it?
Reference
[1] See Scott Meyers, "Mastering User-Defined Conversion Functions," CUJ, August 1995.
Pete Becker is Senior Development Manager for C++ Quality Assurance at Borland International. He has been involved with C++ as a developer and manager at Borland for the past six years, and is Borland's principal representative to the ANSI/ISO C++ standardization committee.