AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

02 Fundamental types / Lesson 4 of 6

Conversions and casts

14 min · +10 XP · 2 exercises · 7 questions

Read the text14 min
Exercises0/2
Quiz7 q

Quick answer

What are the four casts in C++?

C++ has four named casts. static_cast performs ordinary value conversions, such as int to double or an integer to an enum; reinterpret_cast reinterprets a pointer's bits as another pointer type; const_cast adds or removes const; and dynamic_cast is a run-time-checked cast down a polymorphic class hierarchy. Each does one job and is easy to search for, unlike a C-style (int)x.

C++ converts between types constantly and silently. Every mixed-type expression, every argument passed to a differently typed parameter, every if on a number is a conversion. Most are harmless promotions; a handful lose information, and two of those — narrowing a floating value to an integer, and mixing signed with unsigned — are behind a large share of real bugs. This lesson lists the implicit conversions and what each does to a value, shows how brace-initialisation refuses the lossy ones, and explains the four named casts and why the C-style cast has no place in new code.

Where implicit conversions happen

double half = 1 / 2;          // 0.0: the int division happens first, then 0 converts to double
int n = 2.9;                  // 2: truncated, no warning under -Wall
if (n) { }                    // n converts to bool
void f(long long x);
f(n);                         // n widens to long long — a promotion, value preserved

Initialisation and assignment, the operands of an operator, arguments, return values and conditions are all conversion sites. A promotion goes to a wider type and preserves the value; a conversion may not. The compiler reports neither unless you ask (-Wconversion is not part of -Wall).

Narrowing conversions

WrittenStoredWhy
int i = 3.99;3floating → integer truncates toward zero
int i = 3e10;undefinedout of range for int
unsigned u = -1;4294967295signed → unsigned is modular, always defined
int k = 3000000000LL;-1294967296wider → narrower keeps the low bits (modular since C++20)
char c = 300;44300 mod 256
float f = 16777217;16777216.0finteger → float rounds to the nearest representable
bool b = 0.1;trueanything non-zero

Only one row is undefined behaviour — a floating value that does not fit the target integer — but every row loses something. Braces make the compiler say so:

int i{3.99};       // error: narrowing conversion of '3.99' from 'double' to 'int'
unsigned u{-1};    // error
char c{300};       // error
char d{65};        // fine: a constant that fits is not narrowing
long long big = 5;
int j{big};        // error: a run-time long long might not fit, even though this one does

When you want the conversion, say so with static_cast; the reader then knows the loss was considered and -Wall stops second-guessing you.

bool

Zero and null are false; everything else is true. if (x = 5) compiles — assignment, then the 5 converts to true — which is why -Wall warns about an assignment used as a condition and why some people write 5 == x. Going the other way, true is 1 in arithmetic and false is 0; std::cout << std::boolalpha prints the words instead.

char and int

A char in arithmetic becomes an int, which is the whole basis of text processing by hand:

char c = 'q';
int code = c;                            // 113
int digit = '7' - '0';                   // 7
char next = static_cast<char>(c + 1);    // 'r' — c + 1 is an int and must be narrowed back on purpose
bool lower = c >= 'a' && c <= 'z';

Because char is signed here, a byte ≥ 128 is a negative int; before passing one to <cctype> or using it as an index, convert through unsigned char (Module 5 makes this a habit).

Signed and unsigned

int → unsigned wraps modulo 2³², and since C++20 so does the reverse. The trouble is not the conversion itself but that it happens inside comparisons without asking:

unsigned count = 3;
int delta = -5;
std::cout << count + delta << '\n';   // 4294967294 — delta became unsigned
std::cout << (delta < count) << '\n'; // 0 — "-5 < 3" is false, because -5 became 4294967291

The usual arithmetic conversions (lesson 2) convert the signed operand, and GCC's -Wall warns with -Wsign-compare (Clang, the judge's compiler, puts it under -Wextra). The classic sighting is for (int i = 0; i < v.size(); ++i). Fixes, in order of preference:

for (std::size_t i = 0; i < v.size(); ++i)   // match the type
for (int i = 0; i < std::ssize(v); ++i)      // C++20: a signed size
std::cmp_less(delta, count)                  // C++20 <utility>: compares mathematical values — true
std::in_range<int>(someUnsigned)             // does the value fit the target type?

std::cmp_equal, cmp_not_equal, cmp_less, cmp_greater, cmp_less_equal and cmp_greater_equal are the whole family. Casting the unsigned side to long long also works when it is known to fit.

Floating to integer

static_cast<int>(2.9) is 2 and static_cast<int>(-2.9) is -2 — truncation toward zero, the same as std::trunc. If the truncated value does not fit the target, the behaviour is undefined, so static_cast<int>(1e10) is not "some big number", it is a bug. Range-check first, use std::llround when rounding is wanted, and reach for long long when the magnitude is uncertain.

The four casts

double ratio = static_cast<double>(hits) / total;              // numeric; forces floating division
auto level = static_cast<Level>(2);                            // int → enum (lesson 6)
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(&ratio);   // view the object's bytes
void legacyPrint(char* text);
legacyPrint(const_cast<char*>(name.c_str()));                  // only if legacyPrint really does not write
  • static_cast is the everyday cast: every numeric conversion, enum ↔ integer, void* → T*, and up or down a class hierarchy without a run-time check. If a conversion is plausible, static_cast does it.
  • reinterpret_cast reinterprets the bits of a pointer or an integer as another pointer type. It is for byte-level I/O (Module 18) and talking to hardware; almost every other use is undefined behaviour. For values — the bits of a float as a uint32_t — use std::bit_cast.
  • const_cast adds or removes const. Removing it to call a badly declared legacy function is legitimate; writing through the result to an object that was born const is undefined behaviour.
  • dynamic_cast is a checked cast down a polymorphic hierarchy, in Module 10.

Each cast does one thing, is easy to search for, and refuses conversions outside its job.

Why not (int)x

A C-style cast (T)x — and its functional twin T(x) — tries const_cast, then static_cast, then static_cast plus const_cast, then reinterpret_cast, and silently uses the first that compiles. (int)somePointer, (char*)someConstString and (Base*)unrelatedPointer all compile and all mean something different from what a reader expects. It cannot be grepped for, and it keeps compiling when the types around it change into something the cast should have refused. Treat one in new code as a review comment waiting to happen.

Pitfalls

  • double avg = sum / n; with integer operands: the division is integral before the conversion. Cast one operand first.
  • long long area = width * height; where both are int: the product overflows in int before the widening. Cast one operand first.
  • if (index < v.size()) with a negative index: the comparison is unsigned and the bounds check passes.
  • static_cast<int>(hugeDouble) is undefined, not saturated.
  • (int)x where x turns out to be a pointer.

Key takeaways

  • Conversions happen at every assignment, operand, argument, return and condition; promotions preserve values, conversions may not, and -Wall stays quiet.
  • Brace-initialisation rejects narrowing; int x{3.7} and unsigned u{-1} are compile errors.
  • Mixed signed/unsigned expressions convert the signed side; use matching types, std::ssize, or std::cmp_less and friends.
  • Floating → integer truncates toward zero and is undefined when out of range.
  • static_cast for value conversions, reinterpret_cast for bytes and hardware, const_cast for legacy signatures, std::bit_cast for bit views; never a C-style cast.

Common questions

Why should you avoid C-style casts in C++?

A C-style cast (T)x tries const_cast, then static_cast, then reinterpret_cast, and silently uses the first that compiles, so (int)somePointer or (char*)someConstString compile and mean something surprising. It cannot be searched for, and it keeps compiling when the surrounding types change into something a named cast would refuse.

What is a narrowing conversion in C++?

A narrowing conversion is one that can lose information: a floating value to an integer, a wider integer to a narrower one, a signed value to unsigned, or an integer to a floating type that cannot hold it exactly. int i = 3.99; stores 3 without complaint, but brace initialisation, int i{3.99};, makes it a compile error.

Why is -1 < 1u false in C++?

When a signed and an unsigned operand of the same width meet, the usual arithmetic conversions turn the signed one into unsigned, so -1 becomes 4294967295, which is greater than 1. -Wall warns with -Wsign-compare; C++20's std::cmp_less(-1, 1u) compares the mathematical values and returns true.

What happens when you cast a double to an int in C++?

The value is truncated toward zero, so static_cast<int>(2.9) is 2 and static_cast<int>(-2.9) is -2. If the truncated value does not fit in an int, the behaviour is undefined — not saturated — so range-check first, and use std::llround when you want rounding instead.

Why does double avg = sum / n; give a whole number?

When sum and n are integers, the division happens in integer arithmetic first and only the truncated result is converted to double. Cast one operand before dividing: static_cast<double>(sum) / n. The same trap makes long long area = width * height; overflow in int before the widening.

Mark as readSign in to keep your place.

C++ Type Casting: Implicit Conversions and the Four Casts

C++ converts types implicitly at every assignment, operand and argument. Narrowing, signed vs unsigned traps, and when to use each of the four named casts.

  • Course: C++ study plan
  • Module: Fundamental types
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: Clang 18 · C++20

What are the four casts in C++?

C++ has four named casts. static_cast performs ordinary value conversions, such as int to double or an integer to an enum; reinterpret_cast reinterprets a pointer's bits as another pointer type; const_cast adds or removes const; and dynamic_cast is a run-time-checked cast down a polymorphic class hierarchy. Each does one job and is easy to search for, unlike a C-style (int)x.

Lesson

C++ converts between types constantly and silently. Every mixed-type expression, every argument passed to a differently typed parameter, every if on a number is a conversion. Most are harmless promotions; a handful lose information, and two of those — narrowing a floating value to an integer, and mixing signed with unsigned — are behind a large share of real bugs. This lesson lists the implicit conversions and what each does to a value, shows how brace-initialisation refuses the lossy ones, and explains the four named casts and why the C-style cast has no place in new code.

Where implicit conversions happen

double half = 1 / 2;          // 0.0: the int division happens first, then 0 converts to double
int n = 2.9;                  // 2: truncated, no warning under -Wall
if (n) { }                    // n converts to bool
void f(long long x);
f(n);                         // n widens to long long — a promotion, value preserved

Initialisation and assignment, the operands of an operator, arguments, return values and conditions are all conversion sites. A promotion goes to a wider type and preserves the value; a conversion may not. The compiler reports neither unless you ask (-Wconversion is not part of -Wall).

Narrowing conversions

WrittenStoredWhy
int i = 3.99;3floating → integer truncates toward zero
int i = 3e10;undefinedout of range for int
unsigned u = -1;4294967295signed → unsigned is modular, always defined
int k = 3000000000LL;-1294967296wider → narrower keeps the low bits (modular since C++20)
char c = 300;44300 mod 256
float f = 16777217;16777216.0finteger → float rounds to the nearest representable
bool b = 0.1;trueanything non-zero

Only one row is undefined behaviour — a floating value that does not fit the target integer — but every row loses something. Braces make the compiler say so:

int i{3.99};       // error: narrowing conversion of '3.99' from 'double' to 'int'
unsigned u{-1};    // error
char c{300};       // error
char d{65};        // fine: a constant that fits is not narrowing
long long big = 5;
int j{big};        // error: a run-time long long might not fit, even though this one does

When you want the conversion, say so with static_cast; the reader then knows the loss was considered and -Wall stops second-guessing you.

bool

Zero and null are false; everything else is true. if (x = 5) compiles — assignment, then the 5 converts to true — which is why -Wall warns about an assignment used as a condition and why some people write 5 == x. Going the other way, true is 1 in arithmetic and false is 0; std::cout << std::boolalpha prints the words instead.

char and int

A char in arithmetic becomes an int, which is the whole basis of text processing by hand:

char c = 'q';
int code = c;                            // 113
int digit = '7' - '0';                   // 7
char next = static_cast<char>(c + 1);    // 'r' — c + 1 is an int and must be narrowed back on purpose
bool lower = c >= 'a' && c <= 'z';

Because char is signed here, a byte ≥ 128 is a negative int; before passing one to <cctype> or using it as an index, convert through unsigned char (Module 5 makes this a habit).

Signed and unsigned

int → unsigned wraps modulo 2³², and since C++20 so does the reverse. The trouble is not the conversion itself but that it happens inside comparisons without asking:

unsigned count = 3;
int delta = -5;
std::cout << count + delta << '\n';   // 4294967294 — delta became unsigned
std::cout << (delta < count) << '\n'; // 0 — "-5 < 3" is false, because -5 became 4294967291

The usual arithmetic conversions (lesson 2) convert the signed operand, and GCC's -Wall warns with -Wsign-compare (Clang, the judge's compiler, puts it under -Wextra). The classic sighting is for (int i = 0; i < v.size(); ++i). Fixes, in order of preference:

for (std::size_t i = 0; i < v.size(); ++i)   // match the type
for (int i = 0; i < std::ssize(v); ++i)      // C++20: a signed size
std::cmp_less(delta, count)                  // C++20 <utility>: compares mathematical values — true
std::in_range<int>(someUnsigned)             // does the value fit the target type?

std::cmp_equal, cmp_not_equal, cmp_less, cmp_greater, cmp_less_equal and cmp_greater_equal are the whole family. Casting the unsigned side to long long also works when it is known to fit.

Floating to integer

static_cast<int>(2.9) is 2 and static_cast<int>(-2.9) is -2 — truncation toward zero, the same as std::trunc. If the truncated value does not fit the target, the behaviour is undefined, so static_cast<int>(1e10) is not "some big number", it is a bug. Range-check first, use std::llround when rounding is wanted, and reach for long long when the magnitude is uncertain.

The four casts

double ratio = static_cast<double>(hits) / total;              // numeric; forces floating division
auto level = static_cast<Level>(2);                            // int → enum (lesson 6)
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(&ratio);   // view the object's bytes
void legacyPrint(char* text);
legacyPrint(const_cast<char*>(name.c_str()));                  // only if legacyPrint really does not write
  • static_cast is the everyday cast: every numeric conversion, enum ↔ integer, void* → T*, and up or down a class hierarchy without a run-time check. If a conversion is plausible, static_cast does it.
  • reinterpret_cast reinterprets the bits of a pointer or an integer as another pointer type. It is for byte-level I/O (Module 18) and talking to hardware; almost every other use is undefined behaviour. For values — the bits of a float as a uint32_t — use std::bit_cast.
  • const_cast adds or removes const. Removing it to call a badly declared legacy function is legitimate; writing through the result to an object that was born const is undefined behaviour.
  • dynamic_cast is a checked cast down a polymorphic hierarchy, in Module 10.

Each cast does one thing, is easy to search for, and refuses conversions outside its job.

Why not (int)x

A C-style cast (T)x — and its functional twin T(x) — tries const_cast, then static_cast, then static_cast plus const_cast, then reinterpret_cast, and silently uses the first that compiles. (int)somePointer, (char*)someConstString and (Base*)unrelatedPointer all compile and all mean something different from what a reader expects. It cannot be grepped for, and it keeps compiling when the types around it change into something the cast should have refused. Treat one in new code as a review comment waiting to happen.

Pitfalls

  • double avg = sum / n; with integer operands: the division is integral before the conversion. Cast one operand first.
  • long long area = width * height; where both are int: the product overflows in int before the widening. Cast one operand first.
  • if (index < v.size()) with a negative index: the comparison is unsigned and the bounds check passes.
  • static_cast<int>(hugeDouble) is undefined, not saturated.
  • (int)x where x turns out to be a pointer.

Key takeaways

  • Conversions happen at every assignment, operand, argument, return and condition; promotions preserve values, conversions may not, and -Wall stays quiet.
  • Brace-initialisation rejects narrowing; int x{3.7} and unsigned u{-1} are compile errors.
  • Mixed signed/unsigned expressions convert the signed side; use matching types, std::ssize, or std::cmp_less and friends.
  • Floating → integer truncates toward zero and is undefined when out of range.
  • static_cast for value conversions, reinterpret_cast for bytes and hardware, const_cast for legacy signatures, std::bit_cast for bit views; never a C-style cast.

Common questions

Why should you avoid C-style casts in C++?

A C-style cast (T)x tries const_cast, then static_cast, then reinterpret_cast, and silently uses the first that compiles, so (int)somePointer or (char*)someConstString compile and mean something surprising. It cannot be searched for, and it keeps compiling when the surrounding types change into something a named cast would refuse.

What is a narrowing conversion in C++?

A narrowing conversion is one that can lose information: a floating value to an integer, a wider integer to a narrower one, a signed value to unsigned, or an integer to a floating type that cannot hold it exactly. int i = 3.99; stores 3 without complaint, but brace initialisation, int i{3.99};, makes it a compile error.

Why is -1 < 1u false in C++?

When a signed and an unsigned operand of the same width meet, the usual arithmetic conversions turn the signed one into unsigned, so -1 becomes 4294967295, which is greater than 1. -Wall warns with -Wsign-compare; C++20's std::cmp_less(-1, 1u) compares the mathematical values and returns true.

What happens when you cast a double to an int in C++?

The value is truncated toward zero, so static_cast<int>(2.9) is 2 and static_cast<int>(-2.9) is -2. If the truncated value does not fit in an int, the behaviour is undefined — not saturated — so range-check first, and use std::llround when you want rounding instead.

Why does double avg = sum / n; give a whole number?

When sum and n are integers, the division happens in integer arithmetic first and only the truncated result is converted to double. Cast one operand before dividing: static_cast<double>(sum) / n. The same trap makes long long area = width * height; overflow in int before the widening.

Exercises

Caesar shift in char arithmetic

Read an integer k — any sign, any size — then one line of text. Shift every letter k places along the alphabet, wrapping (z + 1 is a) and keeping the case; leave every other character unchanged. Print the shifted line.

Do the arithmetic in int (c - 'a' gives the letter's index) and narrow back to char with static_cast<char>, never a C-style cast. Normalise k to 0…25 first with the floor-modulo idiom ((k % 26) + 26) % 26. Read k with >>, then std::cin.ignore(); before std::getline — the pattern from Module 1.

Input: k, then one line. Output: the shifted line.

Example: input 3 / Hello, World! → Khoor, Zruog!

Compare across the sign divide

Read an integer n, then n lines each holding an int a (possibly negative) and an unsigned b (possibly above 2147483647). For each pair print <a> vs <b>: naive <r1>, correct <r2>, where each result is less, equal or greater.

The naive result is what the built-in < does — it converts a to unsigned first, so compute it with static_cast<unsigned>(a) against b. The correct result compares the mathematical values with std::cmp_less and std::cmp_equal from <utility> (C++20).

Input: n, then n pairs. Output: n lines.

Example: input 2 / -1 1 / 5 5 →

-1 vs 1: naive greater, correct less
5 vs 5: naive equal, correct equal

In this module: Fundamental types

← Floating point — representation, error and printing · const, constexpr and auto →