Type Casting (static_cast & more)
Reviewed & published by Brayan K
Sometimes a value is the wrong type and you need to convert it deliberately. C++ gives you four named casts — static_cast, dynamic_cast, const_cast, and reinterpret_cast — each with a clear purpose. This lesson teaches you which to reach for, why they beat C-style (int) casts, and the truncation traps to watch for.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Use static_cast to convert between numeric types and chars
- Why double -> int truncates, and how to round instead
- Safe downcasting in class hierarchies with dynamic_cast
- Strip const carefully with const_cast (and the danger)
- Reinterpret raw bits with reinterpret_cast (rarely!)
- Why named casts beat C-style (int) casts
💡 Real-World Analogy
Think of the four casts as four different adapters. static_cast is a sensible travel adapter — it connects compatible plugs (an int into a double socket). dynamic_cast is a security guard who checks ID at the door and turns you away if you're not really who you claim to be. const_cast is a master key that removes a "do not touch" sticker — powerful but easy to misuse. reinterpret_cast is a crowbar that forces two unrelated things together by brute reinterpretation of their bits. You reach for the gentlest tool that does the job — almost always static_cast.
📊 The Four Named Casts
| Cast | Use for | Checked? |
|---|---|---|
| static_cast | Related numeric/pointer conversions | Compile time |
| dynamic_cast | Safe downcast in a class hierarchy | Run time |
| const_cast | Add/remove const or volatile | No |
| reinterpret_cast | Reinterpret raw bits/pointers | No |
1. static_cast: the workhorse
This is the cast you'll use 95% of the time. It converts between related types: int ↔ double, char ↔ its numeric code, and pointers within a hierarchy. The classic use is breaking the integer-division trap by casting one operand to double.
#include <iostream>
using namespace std;
int main() {
// The integer-division trap: both ints -> result truncated.
int total = 7, count = 2;
cout << "plain: " << total / count << '\n'; // 3 (wrong for an average)
// static_cast<double> forces floating-point division.
double avg = static_cast<double>(total) / count;
cout << "casted: " << avg << '\n'; // 3.5
// double -> int truncates toward zero (does NOT round).
cout << static_cast<int>(3.9) << '\n'; // 3
cout << static_cast<int>(-3.9) << '\n'; // -3
// char <-> int via its ASCII code.
char ch = 'A';
cout << "code: " << static_cast<int>(ch) << '\n'; // 65
cout << "char: " << static_cast<char>(66) << '\n'; // B
return 0;
}
// ✅ Expected output:
// plain: 3
// casted: 3.5
// 3
// -3
// code: 65
// char: B2. Truncation vs Rounding
Casting a double to int truncates toward zero — it chops the fraction, it does not round. If you want the nearest whole number, round first with std::round (or use std::lround, which returns an integer directly).
#include <iostream>
#include <cmath> // round, lround
using namespace std;
int main() {
double price = 3.7;
// Truncation (cast) vs rounding (round).
cout << "truncate: " << static_cast<int>(price) << '\n'; // 3
cout << "round: " << static_cast<int>(round(price)) << '\n'; // 4
// lround returns a long directly — handy and clear.
cout << "lround: " << lround(2.5) << '\n'; // 3 (rounds half away from zero)
return 0;
}
// ✅ Expected output:
// truncate: 3
// round: 4
// lround: 33. dynamic_cast: safe downcasting
When you have a base-class pointer and need to know the real derived type, dynamic_cast checks at runtime. It returns nullptr for pointers (or throws std::bad_cast for references) when the object isn't the type you asked for. It requires the base class to be polymorphic — at least one virtual function.
#include <iostream>
using namespace std;
struct Animal { virtual ~Animal() = default; }; // virtual -> polymorphic
struct Dog : Animal { void bark() { cout << "Woof!\n"; } };
struct Cat : Animal { void meow() { cout << "Meow!\n"; } };
void speak(Animal* a) {
// dynamic_cast checks the REAL type at runtime.
if (Dog* d = dynamic_cast<Dog*>(a)) d->bark();
else if (Cat* c = dynamic_cast<Cat*>(a)) c->meow();
else cout << "unknown animal\n";
}
int main() {
Dog dog; Cat cat;
speak(&dog); // Woof!
speak(&cat); // Meow!
return 0;
}
// ✅ Expected output:
// Woof!
// Meow!4. const_cast: removing const (carefully)
const_cast adds or removes const. Its main legitimate use is calling an older API that forgot to mark a read-only parameter const. Never use it to actually modify an object that is genuinely const — that's undefined behavior.
#include <iostream>
using namespace std;
// An old API that forgot to mark its parameter const.
void legacyPrint(char* msg) { cout << msg << '\n'; }
int main() {
const char* text = "hello";
// const_cast removes (or adds) const-ness. Use ONLY when you know
// the underlying object is not truly const. Modifying a real const
// through this is undefined behavior.
legacyPrint(const_cast<char*>(text)); // hello
return 0;
}
// ✅ Expected output:
// hello5. reinterpret_cast: raw bits (last resort)
This is the most dangerous cast. It views the same bytes through a completely different type — useful for low-level work like inspecting a value's individual bytes or treating a pointer as an integer address, but non-portable and easy to misuse. Reach for it only when nothing else works.
#include <iostream>
#include <cstdint>
using namespace std;
int main() {
int x = 0x41424344; // bytes: 44 43 42 41 on little-endian
// reinterpret_cast views the SAME bits through a different lens.
unsigned char* bytes = reinterpret_cast<unsigned char*>(&x);
cout << "first byte: " << static_cast<int>(bytes[0]) << '\n';
// 68 (0x44) on a little-endian machine
// Pointer <-> integer address (rarely needed, very low level).
uintptr_t addr = reinterpret_cast<uintptr_t>(&x);
cout << "address stored as integer is non-zero: " << (addr != 0) << '\n'; // 1
return 0;
}
// ✅ Expected output:
// first byte: 68
// address stored as integer is non-zero: 1Your turn. Cast one operand so the percentage keeps its decimals:
#include <iostream>
using namespace std;
int main() {
// 🎯 YOUR TURN — compute an exact percentage.
int correct = 17;
int questions = 20;
// Cast ONE side to double so division keeps the decimals,
// then multiply by 100.
double percent = 100.0 * ___ / questions; // 👉 static_cast<double>(correct)
cout << percent << "%\n";
// ✅ Expected: 85%
return 0;
}🧩 Reorder Challenge
These lines round a price to the nearest cent-free whole number and print it. Reorder so the program prints 4.
A. double price = 3.6;
B. #include <cmath>
C. int rounded = static_cast<int>(round(price));
D. cout << rounded << '\n';
E. int main() {
F. }B. #include <cmath>
E. int main() {
A. double price = 3.6;
C. int rounded = static_cast<int>(round(price));
D. cout << rounded << '\n';
F. }Why: #include <cmath> must precede any use of round. Inside main, declare price first, then round(3.6) gives 4.0 and static_cast<int> converts that cleanly to 4 (rounding before casting avoids truncating 3.6 down to 3). Then print and close the function.
🧠 Quick Recall
Predict the output before revealing each answer.
2 — casting a double to int truncates toward zero; it does not round to 3.
a — 97 is the ASCII code for lowercase 'a', so casting the int to char prints that character.
2.5 — casting one operand to double makes the whole division floating-point.
Pro Tips
- 💡 Default to static_cast: the other three are special-purpose tools.
- 💡 Round, don't truncate: wrap with round() when you want the nearest integer.
- 💡 dynamic_cast needs virtual: the base class must have at least one virtual function.
- 💡 Avoid C-style casts: (int)x hides which conversion is happening and can do something unexpected.
Common Errors (and the fix)
- Expecting rounding from a cast: static_cast<int>(2.9) is 2. Use round() first.
- Casting too late: static_cast<double>(7 / 2) is 3.0 — the division already truncated. Cast an operand: static_cast<double>(7) / 2.
- dynamic_cast on a non-polymorphic type: won't compile. Give the base a virtual destructor.
- Modifying through const_cast: writing to a truly const object is undefined behavior.
📋 Quick Reference
| Cast / Idiom | Use for | Note |
|---|---|---|
| static_cast<T>(x) | related numeric/pointer conversions | the 95% cast; checked at compile time |
| dynamic_cast<T*>(p) | safe downcast in a class hierarchy | runtime check; nullptr on failure |
| const_cast<T>(x) | add / remove const | writing a true const is UB |
| reinterpret_cast<T>(x) | reinterpret raw bits / pointers | low-level, non-portable, last resort |
| static_cast<int>(3.9) | double → int | truncates toward zero → 3 (not 4) |
| static_cast<int>(round(x)) | round then convert | round() / lround() from <cmath> |
| static_cast<double>(a) / b | force floating-point division | cast an operand, not the result |
| static_cast<int>('A') | char → ASCII code | 'A' → 65 |
| static_cast<char>(66) | code → char | 66 → 'B' |
| dynamic_cast needs virtual | base must be polymorphic | at least one virtual function |
| (int)x | C-style cast | avoid — hides which conversion runs |
Mini-Challenge: Grade Calculator
Read points earned and possible, compute a rounded percentage with a cast, and map it to a letter grade. Match the expected output in the comments.
#include <iostream>
#include <cmath>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: grade calculator
// 1. Read pointsEarned (int) and pointsPossible (int).
// 2. Compute the percentage as a double (cast to avoid integer division).
// 3. Round it to the nearest whole percent with round().
// 4. Print the rounded percentage and the letter grade:
// >=90 A, >=80 B, >=70 C, >=60 D, else F
//
// Input: 43 50
// 43/50 = 86% -> grade B
// Expected output: 86% (B)
// your code here
return 0;
}🎉 Lesson Complete
- ✅ static_cast handles related numeric and char conversions
- ✅ double → int truncates; round first for the nearest integer
- ✅ dynamic_cast safely downcasts polymorphic types at runtime
- ✅ const_cast strips const for legacy APIs (never to mutate a true const)
- ✅ reinterpret_cast reinterprets raw bits — a low-level last resort
Practice quiz
What does static_cast<int>(3.9) evaluate to?
- 4
- 3.9
- 3
- 0
Answer: 3. Converting a double to int truncates toward zero — it drops the fraction rather than rounding, so 3.9 becomes 3.
What does static_cast<int>(-3.9) evaluate to?
- -3
- -4
- -3.9
- 3
Answer: -3. Truncation is toward zero, not toward negative infinity, so -3.9 becomes -3 (not -4).
Which cast should you reach for 95% of the time, for related numeric and char conversions?
- reinterpret_cast
- const_cast
- dynamic_cast
- static_cast
Answer: static_cast. static_cast handles well-defined, related conversions and is checked at compile time. It is the everyday workhorse cast.
Which cast is used for safe downcasting in a class hierarchy, checked at runtime?
- static_cast
- dynamic_cast
- const_cast
- reinterpret_cast
Answer: dynamic_cast. dynamic_cast checks the real type at runtime, returning nullptr for pointers (or throwing std::bad_cast for references) on failure.
What does dynamic_cast require of the base class to work?
- At least one virtual function (polymorphic)
- A copy constructor
- A const destructor
- Template parameters
Answer: At least one virtual function (polymorphic). dynamic_cast needs the base class to be polymorphic — at least one virtual function (commonly a virtual destructor).
What does static_cast<double>(7) / 2 evaluate to?
- 3
- 3.0
- 3.5
- 4
Answer: 3.5. Casting one operand to double makes the division floating-point: 7.0 / 2 = 3.5.
What does static_cast<double>(7 / 2) evaluate to?
- 3.5
- 3.0
- 4.0
- 2
Answer: 3.0. The integer division 7 / 2 happens first and truncates to 3; casting that to double gives 3.0. Cast an operand, not the result.
What does static_cast<char>(97) print?
- 97
- 9
- A
- a
Answer: a. 97 is the ASCII code for lowercase 'a', so casting the int to char yields that character.
What is the main legitimate use of const_cast?
- Speeding up loops
- Calling a legacy API that forgot to mark a parameter const
- Converting double to int
- Downcasting pointers
Answer: Calling a legacy API that forgot to mark a parameter const. const_cast strips const to satisfy older non-const APIs. Actually modifying a genuinely const object through it is undefined behavior.
Why prefer named C++ casts over a C-style cast like (int)x?
- They are shorter to type
- They run faster at runtime
- They make intent explicit and let the compiler reject nonsense
- They are required by the standard
Answer: They make intent explicit and let the compiler reject nonsense. A C-style cast silently tries const_cast, static_cast, even reinterpret_cast. Named casts state intent, are searchable, and reject invalid conversions.
Continue this course
- Previous: Strings (std::string)
- Next: Enums & enum class — Named constants and the safer scoped enum class
- Quick reference: C++ cheat sheet
Frequently asked questions
Why should I prefer static_cast over a C-style cast like (int)x?
A C-style cast silently tries everything — const_cast, static_cast, even reinterpret_cast — and picks whatever compiles, which can hide dangerous conversions. static_cast only performs related, well-defined conversions and is easy to search for in code. The named C++ casts make your intent explicit and let the compiler reject nonsense.
What is the difference between static_cast and dynamic_cast?
static_cast converts between related types at compile time with no runtime check. dynamic_cast is used for safe downcasting in class hierarchies: it checks at runtime whether the object really is the target type, returning nullptr (for pointers) or throwing std::bad_cast (for references) if not. dynamic_cast needs at least one virtual function in the base class.
When would I ever use reinterpret_cast?
Almost never in everyday code. reinterpret_cast reinterprets the raw bits of a value as a different unrelated type, for example treating a pointer as an integer address or reading bytes of a struct. It is essential in low-level systems and serialization code but bypasses the type system, so it is easy to get wrong and is non-portable.
Does casting change the original value?
No. A cast produces a new value of the target type; the original variable is untouched. For example static_cast<int>(3.9) yields 3 but the double variable still holds 3.9. The cast only affects the temporary result used in that expression.
Why does static_cast<int>(3.9) give 3 and not 4?
Converting a floating-point value to an integer truncates toward zero — it drops the fractional part rather than rounding. So 3.9 becomes 3 and -3.9 becomes -3. If you want rounding, use std::round (or std::lround) from <cmath> before casting.