Converting Between Types

Dear Computer

Chapter 3: Types

Converting Between Types

A value of one type can sometimes be converted to a different type. Programming languages provide several different pathways for this conversion. A conversion that is implicit and performed automatically by the compiler or interpreter is a coercion. A conversion that is explicit and triggered by the programmer is a cast.

Java orders its numeric primitives by their information capacity in the following way:

  1. double
  2. float
  3. long
  4. int
  5. short
  6. byte

If a value whose type is lower on this list is pressed into a type that is higher, the compiler will implicitly coerce the value. This is a widening conversion. For example, a byte can freely be assigned to an int and a float to a double. Converting in the other direction, a narrowing conversion, is not automatic, as information may be lost. For example, there are many int values that do not fit in a byte. The programmer must acknowledge the risk of this information loss by inserting an explicit cast:

Java
int i = 659;
byte b = (byte) i;

In truth, even an implicit coercion can lead to information loss. Consider this Java program:

Java
int i = 16777217;                 // 2 ^ 24 + 1
float f = i;                      // coerce int to float
System.out.printf("f: %.0f", f);  // prints 16777216

The number 16,777,217 is a legal int, but it cannot be represented as a float. The Java designers consider the loss of lower-order bits in a widening conversion neglible compared to the loss of higher-order bits in a narrowing conversion, so only narrowing conversions require a cast.

Other languages take a firmer stance and perform very few automatic coercions. In Kotlin, for example, even assigning a 4-byte int to a much wider 8-byte long requires an explicit cast:

Kotlin
val small: Int = 100
// val big: Long = small    <- illegal coercion
val big: Long = small.toLong()

C++ provides an extensive casting and coercion system. This code seems like it should be an obvious compilation error:

C++
Square s = 8;

But this code is perfectly legal—provided the class definition of Square includes a converting constructor whose formal parameter matches the right-hand side of the assignment statement, as this definition does:

C++
class Square {
  public:
    Square(int size) : size(size) {}

  private:
    int size;
};

Such implicit coercions are not always considered safe. They can be disabled by qualifying a constructor as explicit:

C++
class Square {
  public:
    explicit Square(int size) : size(size) {}

  private:
    int size;
};

Instantiating a Square must now be done with one of the several explicit construction forms:

C++
// Square s = 8;  <- illegal
Square s = Square(8);
Square t(8);
Square u {8};

This code also seems like it should fail to compile:

C++
Square s {8};
int sum = 10 + s;

Surely a Square cannot be added to an int? Yes, it can, provided the Square class overloads the int cast operator:

C++
class Square {
  public:
    Square(int size) : size(size) {}

    operator int() const {
      return size;
    }

  private:
    int size;
};

Ruby, Python, and JavaScript support a mixture of implicit and explicit conversions. If we want a value to change its type in order to apply different operations, we must cast it. Casting primitives in Python is done with constructor functions:

Python
pint = int(3.14149)
half = float("0.5")
decade = str(1990) + "s"

Ruby provides casting methods:

Ruby
pint = 3.14149.to_i
half = "0.5".to_f
decade = 1990.to_s + "s"

JavaScript provides a mix of constructor functions and methods that perform casting:

JavaScript
pint = Math.trunc(3.14159)
half = Number("0.5")
// 1990.toString()  <- illegal: literals don't have methods
decade = Number(1990).toString() + "s"

Different languages handle failed conversions in different ways. In Java, if we attempt to cast an object to an incompatible class, a ClassCastException is thrown. In C++, we'll get null when converting a pointer or a bad_cast exception when converting a reference. Casting the string "ix" to a float yields an exception in Python, 0.0 in Ruby, and NaN in JavaScript.

← TypecheckingStatic and Dynamic →