14.9. Conversions and Class TypesIn Section 12.4.4 (p. 461) we saw that a nonexplicit constructor that can be called with one argument defines an implicit conversion. The compiler will use that conversion when an object of the argument type is supplied and an object of the class type is needed. Such constructors define conversions to the class type. In addition to defining conversions to a class type, we can also define conversions from the class type. That is, we can define a conversion operator that, given an object of the class type, will generate an object of another type. As with other conversions, the compiler will apply this conversion automatically. Before showing how to define such conversions, we'll look at why they might be useful. 14.9.1. Why Conversions Are UsefulAssume that we want to define a class, which we'll name SmallInt, to implement safe small integers. Our class will allow us to define objects that could hold the same range of values as an 8-bit unsigned charthat is, 0 to 255. This class would catch under- and overflow errors and so would be safer to use than a built-in unsigned char. We'd want our class to define all the same operations as are supported by an unsigned char. In particular, we'd want to define the five arithmetic operators (+, -, *, /, and %) and the corresponding compound-assignment operators; the four relational operators (<, <=, >, and >=); and the equality operators (== and !=). Evidently, we'd need to define 16 operators. Supporting Mixed-Type ExpressionsMoreover, we'd like to be able to use these operators in mixed-mode expressions. For example, it should be possible to add two SmallInt objects and also possible to add any of the arithmetic types to a SmallInt. We could come close by defining three instances for each operator: int operator+(int, const SmallInt&); int operator+(const SmallInt&, int); SmallInt operator+(const SmallInt&, const SmallInt&); Because there is a conversion to int from any of the arithmetic types, these three functions would cover our desire to support mixed mode use of SmallInt objects. However, this design only approximates the behavior of built-in integer arithmetic. It wouldn't properly handle mixed-mode operations for the floating-point types, nor would it properly support addition of long, unsigned int, or unsigned long. The problem is that this design converts all arithmetic types even those bigger than intto int and does an int addition. Conversions Reduce the Number of Needed OperatorsEven ignoring the issue of floating-point or large integral operands, if we implemented this design, we'd have to define 48 operators! Fortunately, C++ provides a mechanism by which a class can define its own conversions that can be applied to objects of its class type. For SmallInt, we could define a conversion from SmallInt to type int. If we define the conversion, then we won't need to define any of the arithmetic, relational, or equality operators. Given a conversion to int, a SmallInt object could be used anywhere an int could be used. If there were a conversion to int, then SmallInt si(3); si + 3.14159; // convert si to int, then convert to double would be resolved by
14.9.2. Conversion OperatorsA conversion operator is a special kind of class member function. It defines a conversion that converts a value of a class type to a value of some other type. A conversion operator is declared in the class body by specifying the keyword operator followed by the type that is the target type of the conversion: class SmallInt { public: SmallInt(int i = 0): val(i) { if (i < 0 || i > 255) throw std::out_of_range("Bad SmallInt initializer"); } operator int() const { return val; } private: std::size_t val; }; A conversion function takes the general form
operator type();
where type represents the name of a built-in type, a class type, or a name defined by a typedef. Conversion functions can be defined for any type (other than void) that could be a function return type. In particular, conversions to an array or function type are not permitted. Conversions to pointer typesboth data and function pointersand to reference types are allowed.
All of the following declarations are errors: operator int(SmallInt &); // error: nonmember class SmallInt { public: int operator int(); // error: return type operator int(int = 0); // error: parameter list // ... }; Although a conversion function does not specify a return type, each conversion function must explicitly return a value of the named type. For example, operator int returns an int; if we defined an operator Sales_item, it would return a Sales_item; and so on.
Using a Class-Type ConversionOnce a conversion exists, the compiler will call it automatically (Section 5.12.1, p. 179) in the same places that a built-in conversion would be used:
Class-Type Conversions and Standard ConversionsWhen using a conversion function, the converted type need not exactly match the needed type. A class-type conversion can be followed by a standard conversion (Section 5.12.3, p. 181) if needed to obtain the desired type. For example, in the comparison between a SmallInt and a double SmallInt si; double dval; si >= dval // si converted to int and then convert to double si is first converted from a SmallInt to an int, and then the int value is converted to double. Only One Class-Type Conversion May Be Applied
For example, assume we had another class, Integral, that could be converted to SmallInt but that had no conversion to int:
// class to hold unsigned integral values
class Integral {
public:
Integral(int i = 0): val(i) { }
operator SmallInt() const { return val % 256; }
private:
std::size_t val;
};
We could use an Integral where a SmallInt is needed, but not where an int is required: int calc(int); Integral intVal; SmallInt si(intVal); // ok: convert intVal to SmallInt and copy to si int i = calc(si); // ok: convert si to int and call calc int j = calc(intVal); // error: no conversion to int from Integral When we create si, we use the SmallInt copy constructor. First int_val is converted to a SmallInt by invoking the Integral conversion operator to generate a temporary value of type SmallInt. The (synthesized) SmallInt copy constructor then uses that value to initialize si. The first call to calc is also okay: The argument si is automatically converted to int, and the int value is passed to the function. The second call is an error: There is no direct conversion from Integral to int. To get an int from an Integral would require two class-type conversions: first from Integral to SmallInt and then from SmallInt to int. However, the language allows only one class-type conversion, so the call is in error. Standard Conversions Can Precede a Class-Type ConversionWhen using a constructor to perform an implicit conversion (Section 12.4.4, p. 462), the parameter type of the constructor need not exactly match the type supplied. For example, the following code invokes the constructor SmallInt(int) defined in class SmallInt to convert sobj to the type SmallInt: void calc(SmallInt); short sobj; // sobj promoted from short to int // that int converted to SmallInt through the SmallInt(int) constructor calc(sobj); If needed, a standard conversion sequence can be applied to an argument before a constructor is called to perform a class-type conversion. To call the function calc(), a standard conversion is applied to convert dobj from type double to type int. The SmallInt(int) constructor is then invoked to convert the result of the conversion to the type SmallInt.
14.9.3. Argument Matching and Conversions
Class-type conversions can be a boon to implementing and using classes. By defining a conversion to int for SmallInts, we made the class easier to implement and easier to use. The int conversion lets users of SmallInt use all the arithmetic and relational operators on SmallInt objects. Moreover, users can safely write expressions that intermix SmallInts and other arithmetic types. The class implementor's job is made much easier by defining a single conversion operator instead of having to define 48 (or more) overloaded operators. Class-type conversions can also be a great source of compile-time errors. Problems arise when there are multiple ways to convert from one type to another. If there are several class-type conversions that could be used, the compiler must figure out which one to use for a given expression. In this section, we look at how class-type conversions are used to match an argument to its corresponding parameter. We look first at how parameters are matched for functions that are not overloaded and then look at overloaded functions.
Argument Matching and Multiple Conversion OperatorsTo illustrate how conversions on values of class type interact with function matching, we'll add two additional conversions to our SmallInt class. We'll add a second constructor that takes a double and also define a second conversion operator to convert SmallInt to double: // unwise class definition: // multiple constructors and conversion operators to and from the built-in types // can lead to ambiguity problems class SmallInt { public: // conversions to SmallInt from int and double SmallInt(int = 0); SmallInt(double); // Conversions to int or double from SmallInt // Usually it is unwise to define conversions to multiple arithmetic types operator int() const { return val; } operator double() const { return val; } // ... private: std::size_t val; };
Consider the simple case where we call a function that is not overloaded: void compute(int); void fp_compute(double); void extended_compute(long double); SmallInt si; compute(si); // SmallInt::operator int() const fp_compute(si); // SmallInt::operator double() const extended_compute(si); // error: ambiguous Either conversion operator could be used in the call to compute:
An exact match is a better conversion than one that requires a standard conversion. Hence, the first conversion sequence is better. The conversion function SmallInt::operator int() is chosen to convert the argument. Similarly, in the second call, fp_compute could be called using either conversion. However, the conversion to double is an exact match; it requires no additional standard conversion. The final call to extended_compute is ambiguous. Either conversion function could be used, but each would have to be followed by a standard conversion to get to long double. Hence, neither conversion is better than the other, so the call is ambiguous.
Argument Matching and Conversions by ConstructorsJust as there might be two conversion operators, there can also be two constructors that might be applied to convert a value to the target type of a conversion. Consider the manip function, which takes an argument of type SmallInt: void manip(const SmallInt &); double d; int i; long l; manip(d); // ok: use SmallInt(double) to convert the argument manip(i); // ok: use SmallInt(int) to convert the argument manip(l); // error: ambiguous In the first call, we could use either of the SmallInt constructors to convert d to a value of type SmallInt. The int constructor requires a standard conversion on d, whereas the double constructor is an exact match. Because an exact match is better than a standard conversion, the constructor SmallInt(double) is used for the conversion. In the second call, the reverse is true. The SmallInt(int) constructor provides an exact matchno additional conversion is needed. To call the SmallInt constructor that takes a double would require that i first be converted to double. For this call, the int constructor would be used to convert the argument. The third call is ambiguous. Neither constructor is an exact match for long. Each would require that the argument be converted before using the constructor:
These conversion sequences are indistinguishable, so the call is ambiguous.
Ambiguities When Two Classes Define ConversionsWhen two classes define conversions to each other, ambiguities are likely: class Integral; class SmallInt { public: SmallInt(Integral); // convert from Integral to SmallInt // ... }; class Integral { public: operator SmallInt() const; // convert from SmallInt to Integral // ... }; void compute(SmallInt); Integral int_val; compute(int_val); // error: ambiguous The argument int_val can be converted to a SmallInt in two different ways. The compiler could use the SmallInt constructor that takes an Integral object or it could use the Integral conversion operation that converts an Integral to a SmallInt. Because these two functions are equally good, the call is in error. In this case, we cannot use a cast to resolve the ambiguitythe cast itself could use either the conversion operation or the constructor. Instead, we would need to explicitly call the conversion operator or the constructor: compute(int_val.operator SmallInt()); // ok: use conversion operator compute(SmallInt(int_val)); // ok: use SmallInt constructor Moreover, conversions that we might think would be ambiguous can be legal for what seem like trivial reasons. For example, our SmallInt class constructor copies its Integral argument. If we change the constructor so that it takes a reference to const Integral class SmallInt { public: SmallInt(const Integral&); }; our call to compute(int_val) is no longer ambiguous! The reason is that using the SmallInt constructor requires binding a reference to int_val, whereas using class Integral's conversion operator avoids this extra step. This small difference is enough to tip the balance in favor of using the conversion operator.
14.9.4. Overload Resolution and Class ArgumentsAs we have just seen, the compiler automatically applies a class conversion operator or constructor when needed to convert an argument to a function. Class conversion operators, therefore, are considered during function resolution. Function overload resolution (Section 7.8.2, p. 269) consists of three steps:
Standard Conversions Following Conversion OperatorWhich function is the best match can depend on whether one or more class-type conversions are involved in matching different functions.
Otherwise, if different conversion operations could be used, then the conversions are considered equally good matches, regardless of the rank of any standard conversions that might or might not be required. On page 541 we looked at the effect of class-type conversions on calls to functions that are not overloaded. Now, we'll look at similar calls but assume that the functions are overloaded: void compute(int); void compute(double); void compute(long double); Assuming we use our original SmallInt class that only defines one conversion operatorthe conversion to intthen if we pass a SmallInt to compute, the call is matched to the version of compute that takes an int. All three compute functions are viable:
Because all three functions would be matched using the same class-type conversion, the rank of the standard conversion, if any, is used to determine the best match. Because an exact match is better than a standard conversion, the function compute(int) is chosen as the best viable function.
Multiple Conversions and Overload ResolutionWe can now see one reason why adding a conversion to double is a bad idea. If we use the revised SmallInt class that defines conversions to both int and double, then calling compute on a SmallInt value is ambiguous: class SmallInt { public: // Conversions to int or double from SmallInt // Usually it is unwise to define conversions to multiple arithmetic types operator int() const { return val; } operator double() const { return val; } // ... private: std::size_t val; }; void compute(int); void compute(double); void compute(long double); SmallInt si; compute(si); // error: ambiguous In this case we could use the operator int to convert si and call the version of compute that takes an int. Or we could use operator double to convert si and call compute(double). The compiler will not attempt to distinguish between two different class-type conversions. In particular, even if one of the calls required a standard conversion following the class-type conversion and the other were an exact match, the compiler would still flag the call as an error. Explicit Constructor Call to DisambiguateA programmer who is faced with an ambiguous conversion can use a cast to indicate explicitly which conversion operation to apply: void compute(int); void compute(double); SmallInt si; compute(static_cast<int>(si)); // ok: convert and call compute(int) This call is now legal because it explicitly says which conversion operation to apply to the argument. The type of the argument is forced to int by the cast. That type exactly matches the parameter of the first version of compute that takes an int. Standard Conversions and ConstructorsLet's look at overload resolution when multiple conversion constructors exist:
class SmallInt {
public:
SmallInt(int = 0);
};
class Integral {
public:
Integral(int = 0);
};
void manip(const Integral&);
void manip(const SmallInt&);
manip(10); // error: ambiguous
The problem is that both classes, Integral and SmallInt, provide constructors that take an int. Either constructor could be used to match a version of manip. Hence, the call is ambiguous: It could mean convert the int to Integral and call the first version of manip, or it could mean convert the int to a SmallInt and call the second version. This call would be ambiguous even if one of the classes defined a constructor that required a standard conversion for the argument. For example, if SmallInt defined a constructor that took a short instead of an int, the call manip(10) would require a standard conversion from int to short before using that constructor. The fact that one call requires a standard conversion and the other does not is immaterial when selecting among overloaded versions of a call. The compiler will not prefer the direct constructor; the call would still be ambiguous. Explicit Constructor Call to DisambiguateThe caller can disambiguate by explicitly constructing a value of the desired type: manip(SmallInt(10)); // ok: call manip(SmallInt) manip(Integral(10)); // ok: call manip(Integral)
14.9.5. Overloading, Conversions, and OperatorsOverloaded operators are overloaded functions. The same process that is used to resolve a call to an overloaded function is used to determine which operator built-in or class-typeto apply to a given expression. Given code such as ClassX sc; int iobj = sc + 3;
there are four possibilities:
Overload Resolution and Operators
Overload resolution (Section 7.8.2, p. 269) for operators follows the usual three-step process:
Candidate Functions for OperatorsAs usual, the set of candidate functions consists of all functions that have the name of the function being used, and that are visible from the place of the call. In the case of an operator used in an expression, the candidate functions include the built-in versions of the operator along with all the ordinary nonmember versions of that operator. In addition, if the left-hand operand has class type, then the candidate set will contain the overloaded versions of the operator, if any, defined by that class.
When resolving a call to a named function (as opposed to the use of an operator), the call itself determines the scope of names that will be considered. If the call is through an object of a class type (or through a reference or pointer to such an object), then only the member functions of that class are considered. Member and nonmember functions with the same name do not overload one another. When we use an overloaded operator, the call does not tell us anything about the scope of the operator function that is being used. Therefore, both member and nonmember versions must be considered.
Conversions Can Cause Ambiguity with Built-In OperatorsLet's extend our SmallInt class once more. This time, in addition to a conversion operator to int and a constructor from int, we'll give our class an overloaded addition operator: class SmallInt { public: SmallInt(int = 0); // convert from int to SmallInt // conversion to int from SmallInt operator int() const { return val; } // arithmetic operators friend SmallInt operator+(const SmallInt&, const SmallInt&); private: std::size_t val; }; Now we could use this class to add two SmallInts, but we will run into ambiguity problems if we attempt to perform mixed-mode arithmetic: SmallInt s1, s2; SmallInt s3 = s1 + s2; // ok: uses overloaded operator+ int i = s3 + 0; // error: ambiguous The first addition uses the overloaded version of + that takes two SmallInt values. The second addition is ambiguous. The problem is that we could convert 0 to a SmallInt and use the SmallInt version of +, or we could convert s3 to int and use the built-in addition operator on ints.
Viable Operator Functions and ConversionsWe can understand the behavior of these two calls by listing the viable functions for each call. In the first call, there are two viable addition operators:
The first addition requires no conversions on either argument s1 and s2 match exactly the types of the parameters. Using the built-in addition operator for this addition would require conversions on both arguments. Hence, the overloaded operator is a better match for both arguments and is the one that is called. For the second addition
int i = s3 + 0; // error: ambiguous
the same two functions are viable. In this case, the overloaded version of + matches the first argument exactly, but the built-in version is an exact match for the second argument. The first viable function is better for the left operand, whereas the second viable function is better for the right operand. The call is flagged as ambiguous because no best viable function can be found.
![]() |