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

💡 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

CastUse forChecked?
static_castRelated numeric/pointer conversionsCompile time
dynamic_castSafe downcast in a class hierarchyRun time
const_castAdd/remove const or volatileNo
reinterpret_castReinterpret raw bits/pointersNo

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: B

2. 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:   3

3. 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:
//    hello

5. 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: 1

Your 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

Common Errors (and the fix)

📋 Quick Reference

Cast / IdiomUse forNote
static_cast<T>(x)related numeric/pointer conversionsthe 95% cast; checked at compile time
dynamic_cast<T*>(p)safe downcast in a class hierarchyruntime check; nullptr on failure
const_cast<T>(x)add / remove constwriting a true const is UB
reinterpret_cast<T>(x)reinterpret raw bits / pointerslow-level, non-portable, last resort
static_cast<int>(3.9)double → inttruncates toward zero → 3 (not 4)
static_cast<int>(round(x))round then convertround() / lround() from <cmath>
static_cast<double>(a) / bforce floating-point divisioncast an operand, not the result
static_cast<int>('A')char → ASCII code'A' → 65
static_cast<char>(66)code → char66 → 'B'
dynamic_cast needs virtualbase must be polymorphicat least one virtual function
(int)xC-style castavoid — 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

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

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.

Related lessons