11.4. Conversion Operators

Time: 00:04:17 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review

Data types are essential to computer programs, determining how much memory to allocate for a value and how to interpret its bits. For example, 2 and 2.0 are mathematically the same, but their computer representations are very different. The former is typically half as long as the latter and is represented in simple binary, while the latter is represented in the IEEE 754 format. Among the many ways computer scientists classify programming languages are as strongly or weakly typed. Aside from type deduction, programmers must specify a variable's type and a function's return type. Even expressions have a type derived from their data and operations. Every value in a strongly typed language has a data type, and the language rigorously enforces type compatibility. Two types are compatible if a program can combine them without modification. Weakly typed languages permissively allow most combinations, attempting automatic type conversions as needed.

Strong- and weak-typing have their relative advantages and disadvantages. Strong typing can catch some programming errors at compile time, while weak typing may result in runtime errors or inappropriate automatic type conversions. Alternatively, there are often cases where converting a value represented by one data type into a value represented by a different type is necessary. Some strongly typed languages limit or prevent type conversions, leading to awkward or convoluted programs. Although C++ leans toward strong typing, it does perform some automatic type conversions and allows programmers to perform other conversions explicitly.

C++ Fundamental Type Conversions

There are often cases where converting a value represented by one data type into a value represented by a different type is necessary. What the conversions mean and how C++ implements them depends on the data types. For example, the types int and double are different kinds of numbers, and converting them is a meaningful operation. Many languages automatically convert an int to a double but require programmers to explicitly perform the opposite conversion due to the potential loss of precision. Other type conversions are also possible and often necessary. For example, 2, '2,' and "2" represent an integer, a character, and a string, respectively. Weakly typed languages generally convert one type to another automatically, but more strongly typed languages require explicit programmer intervention.

C++ further allows converting between objects and fundamental types with conversion constructors and operators. Programmers can also use these functions to convert between objects instantiated from different classes.

Conversion Constructor

Chapter 9 introduced conversion constructors, which we review here with a more recent example.

fraction(int n = 0, int d = 1); fraction f; fraction f(5); fraction f(2, 3);
(a)(b)(c)(d)
The fraction conversion constructor. The fraction conversion constructor converts one, two, or three integers to a fraction object (i.e., it creates a fraction object with 1-3 integers). Although programmers can implement the constructor with three functions, only one is needed when implemented with default arguments.
  1. The fraction constructor implemented as a part of the earlier example. With two default arguments, programs can call the constructor three ways.
  2. Creates an "empty" fraction object representing 0/1.
  3. Converts 5 into the fraction object representing 5/1.
  4. Converts 2 and 3 into the fraction object representing 2/3.

Conversion Operator

Conversion constructors are not the only conversion mechanism C++ programmers can use. C++ also allows programmers to overload the casting operator. The fraction class doesn't need a conversion operator, so we base the following examples on the Time class, which has three integer member variables: hours, minutes, and seconds. The first step in the Time add function is converting two Time objects into integers that represent the time stored in each object as seconds.

Time Time::add(Time t2)
{
    int i1 = hours * 3600 + minutes * 60 + seconds;
    int i2 = t2.hours * 3600 + t2.minutes * 60 + t2.seconds;

    return Time(i1 + i2);
}
The Time add function. The variable i1 represents the total time stored in the implicit or this object, while the variable i2 represents the total time stored in the parameter t2.

Programmers can create an overloaded conversion operator that converts an instance of Time into an integer and replace the expressions calculating i1 and i2 in the previous example.

operator int() { return hours * 3600 + minutes * 60 + seconds; }
Overloaded Time conversion operator. Like constructors, conversion operators are always members (i.e., not friends) and do not have an explicit return type (the function name implies the type, which, in this example, is int). The example illustrates the conversion operator implemented inside the Time class. However, the original Time conversion constructor and the overloaded conversion operator conflict, making it impossible for the class to have both functions.

Based on the overloaded conversion operator, it's possible to rewrite the Time add function. We also take this opportunity to change the add function into the overloaded operator+, which is demonstrated both as a member and as a friend. Unfortunately, neither is the final version of operator+ (see "Conversion Conflicts" in the red highlighted box below).

Memberfriend
Time Time::operator+(Time t2)
{
    int i1 = (int)*this;
    int i2 = (int)t2;
    int s = i1 + i2;
    int h = s / 3600;
    s %= 3600;

    return Time(h, s / 60, s % 60);
}
Time operator+(Time t1, Time t2)
{
    int i1 = (int)t1;
    int i2 = (int)t2;
    int s = i1 + i2;
    int h = s / 3600;
    s %= 3600;

    return Time(h, s / 60, s % 60);
}
(a)
int i1 = int(*this);
int i2 = int(t2);
int i1 = int(t1);
int i2 = int(t2);
(b)
int i1 = *this;
int i2 = t2;
int i1 = t1;
int i2 = t2;
(c)
Overloaded conversion operator example. Programs can call overloaded conversion operators with casting or functional notation. The example demonstrates conversion operators with member and friend versions of the Time addition operator. The member version requires explicitly using the this pointer, dereferenced, to access the bound or calling object's member variables. The conversion constructor and operator conflict (detailed below), so the function extracts the hours, minutes, and seconds from the sum before calling the three-argument constructor.
  1. Casting notation: the parentheses form the casting operator, surrounding the new or destination type and preceding the operand.
  2. Functional notation: the new or destination type forms the function name, and the operand is the argument in the parentheses.
  3. Automatic conversion: beginning with the C++11 standard, type conversions may occur automatically without an explicit call to the casting operator.

Conversions: Constructor vs. Operator

Programs sometimes need to convert an object from one class type to another or between a fundamental type and an object. Various situations may dictate when programmers use a constructor or an operator to perform the conversion. In some ways, we can consider one technique the opposite of the other - think of constructors pulling data into an object and operators pushing it out.

Object To ObjectInteger To Object
class foo
{
    public:
        foo(bar b);     // bar → foo
};
class foo
{
    public:
        foo(int i);     // int → foo
};
(a)(b)
Conversion constructor applications. Conversion constructors, like all constructors, create new objects. However, in the case of conversion constructors, it's convenient to think of them as converting values from one datatype to another. To add a conversion constructor, programmers must "own" (i.e., be able to edit) the class specification of the class "pulling" data into the object, foo in this example.
  1. The constructor converts an object from one class type to another, from a bar to a foo.
  2. The constructor converts a fundamental type to an object, an int to a foo.
Object To Object Object To Integer
class bar
{
    public:
        operator foo();	// bar → foo
};
class bar
{
    public:
        operator int();	// bar → int
};
(a)(b)
Conversion operator applications. Applied to fundamental datatypes, C++ uses parentheses as the casting operator. C++ allows classes to overload the casting operator to convert instances of the class to other classes or fundamental types. To add a conversion or casting operator, programmers must "own" (i.e., be able to edit) the class specification of the class "pushing" data out of the object, bar in this example. While both constructors and operators can convert objects from one class type to another, only operators can convert an object to a fundamental type.
  1. The constructor converts an object from one class type to another, from a bar to a foo.
  2. The constructor converts a fundamental type to an object, a foo to an int.
class Time
{
    public:
        Time(int t);                     // (a)
        int();                           // (b)
        friend operator+(Time, Time);
};
class fraction
{
    private:
        fraction(int n = 0, int d = 0);                        // (a)
        double() { return (double) numerator / denominator; }  // (b)
        double operator+(fraction, fraction);
};
Conversion conflicts. Unfortunately, in most cases, a class can't simultaneously define both a conversion constructor (a) and a conversion operator (b). The following expressions demonstrate the ambiguity problem by describing two ways the compiler can evaluate them.
T + 30
Let T be an instance of Time
  1. Convert 30 to a Time object with the conversion constructor, and complete the expression with the Time addition operator.
  2. Convert T to an int with the conversion operator, and complete the evaluation with the "normal" addition operator.
F + 5
Let F be a fraction object
  1. The compiler can call the constructor, converting the 5 to a fraction object
  2. The compiler can call the conversion operator, converting F to a double, promote the 5 to 5.0, and then sum the resulting values
The ambiguities cause fatal compile-time errors, and the programs do not compile. Programmers may resolve the conflict by removing operator+