Overloaded operators and conversions-conversions and class types [on]
introduction:
We mentioned earlier that an implicit conversion can be defined with a non-explicit constructor called by an argument. The compiler will use this conversion when an object of the actual parameter type is needed for an object of class type. So: this constructor defines a conversion to a class type.
In addition to defining conversions to class types, you can also define conversions from class types to other types. That is: we can define a conversion operator, given an object of a class type, the operator will produce other types of objects. As with other conversions, the compiler will automatically apply this conversion.
Why are conversions useful?
Define a SmallInt class that implements safe small integers. This class will enable us to define objects to hold values in the same range as 8-bit unsignedchar, namely: 0 to 255. This class can catch underflow and overflow errors, so it is safer to use than the built-in unsignedchar.
We want this class to define all operations supported by unsignedchar. Specifically, we want to define 5 arithmetic operators (+,-, *, /,%) and their corresponding compound assignment operators, 4 relational operators (<, <=,>,> =), and The equality operators (==,! =). Obviously, 16 operators%> _ <% need to be defined.
1. Support mixed type expressions
Furthermore, we hope that these operators can be used in mixed-mode expressions. For example, it should be possible to add two SmallInt objects, or you can add any arithmetic type to SmallInt. The goal is achieved by defining three instances for each operator:
int operator + (int, const SmallInt &);
int operator + (const SmallInt &, int);
SmallInt operator + (const SmallInt &, const SmallInt &);
However, this design is only close to the behavior of built-in integer arithmetic. It cannot be applied to mixed mode of floating-point types, nor can it properly support the addition of long, unsignedint, or unsignedlong.
Conversion reduces the number of operators required
C ++ provides a mechanism: a class can define its own conversion and apply it to objects of its class type. For SmallInt, you can define a conversion from SmallInt to int. If the conversion is defined, there is no need to define any arithmetic, relational or equality operators (otherwise 48 must be defined!). Given a conversion to int, the SmallInt object can be used wherever int values are available.
If there is a conversion to int, the following code:
SmallInt si (3);
/ ** There can be a conversion like this:
* 1. Convert si to int value
* 2. Convert the resulting int to a double value and add it to the double precision constant 3.14159,
* Get double value
* /
si + 3.1415926;
Second, the conversion operator
The conversion operator is a special class member function ((⊙o⊙) is really special!): It defines a conversion that converts a class type value to another type value. The conversion operator is declared in the body of the class definition, followed by the reserved operator followed by 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;
};
The conversion function takes the following general form:
operator type ();
Here, type represents a built-in type name, a class type name, or a name defined by a type alias. Conversion functions can be defined for any type that can be used as a function return type (except void). In general, conversion to array or function types is not allowed, conversion to pointer types (data and function pointers) and reference types is possible.
【note】
The conversion function must be a member function, no return type can be specified, and the parameter list must be empty.
Although conversion functions cannot specify a return type, each conversion function must explicitly return a value of the specified type. For example, operatorint returns an int value; if operatorSales_item is defined, it will return a Sales_item object, and so on.
【Best Practices】
Conversion functions should generally not change the object being converted. Therefore, conversion operators are usually defined as const members!
1.Use class type conversion
As long as there is a conversion, the compiler will automatically call it where the built-in conversion can be used:
1) In the expression:
SmallInt si;
double dval;
si> = dval; // si is converted to int, then they are converted to double
2) In the condition:
if (si) // si is converted to int, then they are converted to bool
{
// ...
}
3) Pass arguments to or return values from functions:
int calc (int);
SmallInt si;
calc (si); // Si converted to int, and then call the function calc
4) Operands as overloaded operators:
cout << si << endl; // si is converted to int, then call opreator <<
5) In explicit type conversion:
int ival;
SmallInt si = 3.14;
// Explicitly convert si to int
ival = static_cast (si) + 3;
Class type conversion and standard conversion
When using conversion functions, the type being converted does not have to exactly match the type required. If necessary, you can keep up with standard conversions to get the type you want.
SmallInt si;
double dval;
si> = dval; // si converts to int and standard conversion to double
3. Only one class type conversion can be applied
You cannot convert another class type after the type conversion. If multiple class type conversions are required, the code is wrong!
class Intergral
{
public:
Intergral (int i): val (i) {}
operator SmallInt () const
{
return val% 256;
}
private:
std :: size_t val;
};
You can use Intergral where you need SmallInt, but you cannot use Intergral where you need int:
int calc (int);
Intergral intVal;
SmallInt si (intVal); // OK: intVal converted to SmallInt
int i = calc (si); // OK: si is converted to int
int j = call (intVal); // Error
In the final calc call: there is no direct conversion from Integral to int. Two type conversions are required from int: first from Integral to SmallInt, then from SmallInt to int. However, the language only allows class type conversions once, so the call goes wrong.
4. Standard conversion can be placed before class type conversion
When using a constructor to perform an implicit conversion, the parameter type of the constructor does not have to exactly match the type provided.
void calc (SmallInt);
short sobj;
/ *
* Call the constructor (SmallInt (int)) defined in the SmallInt class,
* Convert sobj to SmallInt type
* /
calc (sobj);
If necessary, before calling the constructor to perform the class type conversion, a standard conversion sequence can be applied to the actual parameters. In order to call the function calc (), a standard conversion is applied to convert dobj from a double type to an int type, then the constructor SmallInt (int) is called to convert the conversion result to a SmallInt type.
void calc (SmallInt);
double dval;
calc (dval);
// The effect is equivalent to
// calc (static_cast (dval));
// P457 exercise 14.40
class Sales_item
{
public:
Sales_item (const std :: string & book = ""):
isbn (book), units_sold (0), revenue (0.0) {}
/ **
* In fact, it is not a good idea to define the conversion operators of string and double
* Because it is generally unnecessary to use Sales_item objects where string and double are needed
* /
operator string () const
{
return isbn;
}
operator double () const
{
return revenue;
}
// As before ...
private:
std :: string isbn;
unsigned units_sold;
double revenue;
};
// Exercise 14.42
class CheckoutRecord
{
public:
typedef unsigned Date;
operator bool () const
{
return wait_list.empty ();
}
// As Before ...
private:
// As Before ...
vector <pair *> wait_list;
};
Third, actual parameter matching and conversion
Although class type conversion may be a benefit of implementing and using classes, class type conversion may also be a major source of compile-time errors! When there are multiple ways to convert from one type to another, if there are several class type conversions available, the compiler must decide which one to use for a given expression.
[Beware of mines]
If used with care, class type conversion can greatly simplify class code and user code. If used too freely, class type conversions can cause confusing compile-time errors, which are difficult to understand and hard to avoid!
1.Parameter matching and multiple conversion operators
class SmallInt
{
public:
// Convert from int / double to SmallInt
SmallInt (int = 0);
SmallInt (double);
// Convert from SmallInt to int / double
operator int () const
{
return val;
}
operator double () const
{
return val;
}
private:
std :: size_t val;
};
[Beware of mines]
In general, it is bad practice to give a conversion between a class and two built-in types!
Consider the simplest case of calling a non-overloaded function:
void compute (int);
void fp_compute (double);
void extended_compute (long double);
SmallInt si;
compute (si); // OK
fp_compute (si); // OK
extended_compute (si); // Error
In this program, any conversion operator can be used in the compute call:
1) operatorint produces an exact match on the parameter type.
2) First call operatordouble for conversion, followed by the standard conversion from double to int to match the parameter type.
Since the exact match conversion is better than other conversions that require standard conversion, the first conversion sequence is better. Select the conversion function SmallInt :: operatorint () to convert the actual parameters.
In the second call, fp_compute can be called with either transformation. However, the conversion to double is an exact match and no additional standard conversion is required.
The last call to extended_compute is ambiguous. You can use either conversion function, but each must follow a standard conversion to get a longdouble. Therefore, no conversion is better than the other, and the call is ambiguous.
【summary】
If both conversion operators are available in one call, and there is a standard conversion after the conversion function, choose the best match based on the category of that standard conversion!
2.Parameter matching and constructor conversion
Just as there may be two conversion operators, there may also be two constructors that can be used to convert a value to the target type.
void manip (const SmallInt &);
double d;
int i;
long l;
manip (d); // OK
manip (i); // OK
manip (l); // Error
In the first call, you can use any constructor to convert d to a value of type SmallInt: the int constructor requires a standard conversion of d, and the double constructor matches exactly. Because the exact match is better than the standard conversion, the conversion is done with the constructor SmallInt (double).
In the second call, the constructor SmallInt (int) provides an exact match. Calling the SmallInt constructor that accepts a double argument requires first converting i to the double type. For this call, the compiler prefers to use the int constructor to convert the arguments.
The third call is ambiguous. No constructor matches exactly long. The actual arguments need to be converted before using each constructor:
1) Standard conversion (from long to double) followed by SmallInt (double).
2) Standard conversion (from long to int) followed by SmallInt (int).
These conversion sequences are indistinguishable, so the call is ambiguous.
【summary】
When both constructor-defined conversions are available, if there is a standard conversion required by the constructor arguments, the type of the standard conversion is used to select the best match.
3. Ambiguity when two classes define conversion
When two classes define mutual conversion, there is likely to be ambiguity:
class Integral;
class SmallInt
{
public:
SmallInt (Integral);
// ...
};
class Integral
{
public:
operator SmallInt () const;
// ...
};
void compute (SmallInt);
Integral int_val;
/ **
* Error: The call is ambiguous
* But some compilers are still undetectable, such as the g ++ compiler I tested
* /
compute (int_val);
The argument int_val can be converted into a SmallInt object in two different ways. The compiler can accept the constructor of the SmallInt object (the Chinese version here is incorrectly translated, the original is: ... It could use the Integral conversion operation that converts an Integral to a SmallInt ..., so it should be translated as the constructor of the SmallInt object, not the Integral object's constructor), or you can use the Integral conversion operation of the Integral object to convert to the SmallInt object. Because these two functions are not superior, so this call will go wrong!
In this case, you cannot use explicit type conversion to solve the ambiguity-explicit type conversion itself can use both conversion operations and constructors.On the contrary, you need to explicitly call conversion operators or constructors:
compute (SmallInt (int_val)); // OK
compute (int_val.operator SmallInt ()); // OK
Moreover, for some seemingly insignificant reasons, we believe that there may be a legally ambiguous conversion. For example, the constructor of the SmallInt class copies its Integral argument, if you change the constructor to accept a constIntegral reference:
class SmallInt
{
public:
SmallInt (const Integral &);
// ...
};
The call to compute (int_val) is no longer ambiguous! The reason is that using the SmallInt constructor requires binding a reference to int_val, and using the conversion operator of the Integral class can avoid this extra step. This small difference is enough to make us inclined to use conversion operators.
【Best Practices】
The best way to avoid ambiguity is to avoid writing paired classes that provide implicit conversions to each other!
[Warning: Avoid excessive use of conversion functions. P460 is a wonderful story! 】
The best way to avoid ambiguity is to ensure that there is at most one way to convert one type to another. The best way to do this is to limit the number of conversion operators. In particular, there should be only one conversion to a built-in type.