Skip to content
Theory · Casts and Aliases · lesson 1 of 7

Implicit conversions and static_cast

What you'll learn

What you'll learn:

  • predict which common type the compiler converts the operands to;
  • explain why a negative int turns into a huge unsigned;
  • pick the named cast for the job: static_cast, const_cast or reinterpret_cast;
  • give a type a second name with using.
An odometer unsigned wound back by -5, its wheels showing 4294967294

Think of the mechanical odometer in an old car: a row of digit wheels and no minus sign anywhere. Wind it back from 00000 and it shows 99999, because it has nothing below zero to show. The unsigned type works the same way: it has no negative numbers, and one step down from zero takes it to its largest value.

The sum of 3 and -5 is -2, but this program prints a very different number:

C++
#include <iostream>

int main() {
    int change = -5;
    unsigned stock = 3u;
    std::cout << stock + change << '\n';  // 4294967294
}

Before the addition, the compiler converted both operands to one type, and that type turned out to be unsigned. In it, -5 became a huge number, like an odometer wound five steps back from zero. That is why the sum came out huge as well.

Implicit conversions

An implicit conversion is a type change that the compiler makes on its own, without being asked in the code. Before arithmetic and comparisons, it brings the operands to a common type by two rules:

  1. bool, char and short are first promoted to int: true + true is an int with the value 2.
  2. If the types still differ, both operands are converted to a common type.

The common type for frequent pairs:

  • int and double give double;
  • int and long long give long long;
  • int and unsigned give unsigned.

Together these rules are called the usual arithmetic conversions.

A negative int as unsigned

A negative int value converted to unsigned wraps around, like the odometer, and the standard defines the result. -1 becomes the largest unsigned value, and -5 becomes the number four below it.

The largest 4-byte unsigned value is 4294967295, so -5 becomes 4294967291, and 3 + 4294967291 gives the 4294967294 from the first example.

Change x with the buttons and watch the top bit: while it is 1, int reads the number as negative and unsigned reads it as huge.

LiveSame bits, two numbersint -5 · unsigned 4294967291
int x = -5;

bit 31: worth -2147483648 in an int, +2147483648 in an unsigned

int-5
unsigned4294967291

The top bit is 1. An int counts it as -2147483648 and an unsigned as +2147483648, so as an unsigned the same bits are 4294967296 + (-5) = 4294967291.

Watch the top bit

The analogy is off in one way: the odometer's wheels are decimal, while unsigned stores bits, so its maximum is not 99999 but 4294967295.

Comparisons work the same way: < with an int operand and an unsigned one compares the converted values. Interviewers like to ask about this trap.

Narrowing conversions

A negative int turned unsigned at least has a defined result. Converting to a narrower type is riskier:

  • a double value stored in an int loses its fraction;
  • a long long value may not fit in an int.

These are called narrowing conversions. Written with =, they compile without an error, while braces refuse them, as we saw in the topic on variables.

Casting with static_cast

When the compiler will not make a needed conversion on its own, you write it in the code explicitly. Such an expression is called a cast: static_cast<double>(sum) gives the value of sum as a double, while the variable itself stays an int.

C++
#include <iostream>

int main() {
    int sum = 7;
    int count = 2;
    std::cout << static_cast<double>(sum) / count << '\n';  // 3.5
}

The dividend is now a double, so count is converted to double too, and the division keeps the fraction. In the other direction, static_cast<int>(2.9) gives 2: the fraction is dropped.

The same technique guards against overflow. The constants INT_MIN and INT_MAX from the <climits> header hold the smallest and largest int values:

C++
#include <climits>
#include <iostream>

int main() {
    int a = INT_MAX;
    int b = 1;
    long long sum = static_cast<long long>(a) + b;
    std::cout << sum << '\n';  // 2147483648
}

In int, the sum a + b would overflow, and that is undefined behavior. Once a is cast to long long, b goes to the same type, and the numbers are added in the wider type.

In C, a type is changed with the notation (double)sum. It works in C++ too, but it can also remove const or reinterpret a pointer. The code does not show which of these was meant. A named cast such as static_cast has one task, and its name lets you find every place in the code where it is used.

Standard library types define conversions of their own: a string literal implicitly becomes a std::string, and a std::string becomes a std::string_view. The author of a type writes such rules, and the compiler applies them implicitly as well.

Common mistake

The cast is applied to the result of the division instead of an operand:

C++
#include <iostream>

int main() {
    int sum = 7;
    int count = 2;
    double avg = static_cast<double>(sum / count);  // ✗
    std::cout << avg << '\n';  // 3
}

The program prints 3. The parentheses hold the integer division 7 / 2, and only its result 3 becomes a double. Cast an operand instead: static_cast<double>(sum) / count gives 3.5.

Key points

  • Before arithmetic and comparisons, the compiler converts the operands to a common type; for int and unsigned it is unsigned.
  • A negative int converted to unsigned wraps around: -1 becomes 4294967295.
  • static_cast<T>(x) changes the type explicitly; cast an operand, not the result of the division.
  • A named cast has one task and stands out in the code, unlike (double)sum.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way