C++ has more ways to convert one type into another than most languages, and that's not an accident. Each cast tells the compiler something different about the intent and how much safety to enforce. This lesson covers the implicit conversions that happen on their own, why the old C-style (int)x cast is error-prone, and the four named casts C++ provides as replacements.
C++ does a fair amount of type conversion automatically, without any cast in the source. Some of those conversions are harmless. Some lose data. Knowing which is which is the foundation everything else in this lesson sits on.
The two broad categories are promotions (smaller into bigger, no loss) and narrowing conversions (bigger into smaller, possible loss). The compiler does promotions without complaint. It usually does narrowing the same way, with a warning at best, which is why explicit casts exist for the cases that are intentional.
Assigning an int to a double, or passing an int where a double is expected, causes the compiler to widen the value. Every int fits in a double, so nothing is lost.
The first line shows 4 printed without a decimal part. The value is a double, but std::cout drops trailing zeros by default. The second line is the interesting one: itemCount * averagePrice mixes an int with a double. C++ promotes the int to a double first, then does the multiplication in double, so the result is 79.96 rather than 79.
This promotion rule is what makes the "average price" trick work. Dividing a double total by an int count promotes the count and keeps the division in floating point.
If both operands had been int, the division would have been integer division and the decimal part would have been thrown away. The promotion is what keeps the math honest.
A narrowing conversion goes the other way: a bigger or more precise type squeezed into a smaller one. C++ allows many narrowing conversions without complaint, which is the source of more bugs than any other implicit behavior in the language.
4.7 became 4. C++ truncates toward zero, it doesn't round. For 5 stars, round explicitly with std::round(rating) from <cmath> and then convert.
The compiler may emit a warning on this assignment with -Wconversion enabled, but by default it just does it. Brace initialization is the one place C++ refuses.
Inside { ... }, the language forbids narrowing. That's one of the small but useful guarantees of modern C++ initialization.
Any built-in arithmetic type or pointer converts to bool implicitly. The rule is simple: zero (or the null pointer) becomes false, everything else becomes true.
The conversion runs the other way too. A bool converts to int, with false becoming 0 and true becoming 1. This shows up in code that counts successes, but a plain counter is usually clearer than relying on the conversion.
Before C++ got its named casts, the only way to force a conversion was the C-style cast: (int) value. The syntax is simple, which is part of the problem. It does too much, and it doesn't communicate intent.
When the compiler sees (int) x, it tries each of the following in order, and the first one that works wins:
const_cast.static_cast.static_cast followed by a const_cast.reinterpret_cast.reinterpret_cast followed by a const_cast.The selection isn't visible at the call site. The cast that compiles is the one applied, even if a different one was intended. That makes (int) x both a legitimate numeric conversion and a bit-level reinterpretation, depending on what x is.
An example where the C-style cast does the wrong thing.
The cast looks innocent. It compiles. It might even run and print wireless Mouse. But productName points to a string literal, which lives in read-only memory on most platforms, and modifying it is undefined behavior. The program might crash, might corrupt other data, or might appear to work. The compiler didn't signal which kind of cast was happening because C-style casts don't distinguish.
The named casts in C++ each do one job and refuse to do the others. That's the point of having four of them.
C++ replaces the all-purpose C-style cast with four distinct keywords:
| Cast | Purpose | Safety level |
|---|---|---|
static_cast<T>(x) | Numeric conversions, related-type conversions, void pointer back to typed pointer | Compile-time checked |
dynamic_cast<T*>(p) | Safe downcasting in polymorphic hierarchies | Runtime checked |
const_cast<T>(x) | Adds or removes const (or volatile) | Compile-time only, runtime UB on misuse |
reinterpret_cast<T>(x) | Bit-level reinterpretation between unrelated types | No check, almost no safety |
Three things make these better than (T)x:
reinterpret_cast and every dangerous spot shows up. There's no clean grep for (T).(T)x, which is intentional. Bypassing the type system should require some effort.The rest of this lesson walks through each one.
static_cast<T>(x)static_cast is the workhorse. Use it for numeric conversions, conversions between related types, and any conversion that the compiler can fully check at compile time.
The most common use is forcing the kind of conversion that would otherwise happen implicitly, but making the intent visible. The classic case is "average price": dividing a total by an item count.
In this case the static_cast is technically redundant, because cartTotal / itemCount already promotes itemCount to double. But the cast tells a reader that one operand is integer and the division is meant to happen in floating point. That's worth a few extra characters in shared code.
The case where the cast really matters is the other direction: chopping a double down to an int for a star rating.
Same truncation as the implicit version. The difference is that the cast signals data loss as expected here.
What's wrong with this code?
The first cast is fine. The second one casts the result of an integer division. itemsInCart / customers is 7 / 3, which is already 2 in int arithmetic, and the cast does nothing useful. The decimal part was lost before the cast ran. The cast hides the bug by making the line look like a deliberate conversion.
Fix:
Promote one operand first so the division runs in floating point, then store the result in a double. The cast is now doing real work: it changes the type of one operand before the operation, not after it.
static_cast also handles conversions between related pointer types, like a base class pointer to a derived class pointer. It's only checked at compile time, so it doesn't verify the object's actual type at runtime. Casting a pointer to the wrong derived type and using it results in undefined behavior.
This works because asBase really did point to a DigitalProduct. If it had pointed to some other Product, the static_cast would still have compiled, and accessing downloadCount would have been undefined behavior. For checked downcasting, use dynamic_cast.
void* Back to a Typed PointerC APIs sometimes return a void* that originally pointed to a specific type. static_cast brings it back.
The cast trusts that the void* really points to an int. There's no runtime check. If the original pointer was something else, reading through typed is undefined behavior.
dynamic_cast<T*>(p)dynamic_cast is the safe way to downcast in a polymorphic class hierarchy. Unlike static_cast, it checks the actual type of the object at runtime. If the cast is invalid, it gives a clean failure instead of undefined behavior.
For dynamic_cast to work, the base class needs at least one virtual function. That's what makes the class polymorphic and gives the runtime a way to identify the actual derived type. Without virtual, dynamic_cast won't compile.
For this lesson, the focus is the cast itself: what it does and how it reports failure.
The first cast worked because item really points to a DigitalProduct. The second one failed because it doesn't point to a PhysicalProduct, so dynamic_cast returned a null pointer. The if check catches the failure cleanly.
The reference form of dynamic_cast can't return null (a reference can't be null), so it reports failure by throwing std::bad_cast instead.
The choice depends on the failure-handling strategy. The pointer form is convenient when a different code path can take over. The reference form is right when the type is a precondition and a wrong type is a bug worth crashing on.
dynamic_cast walks the type information at runtime, which is slower than static_cast. In hot loops, prefer designs that avoid downcasting (virtual functions, visitor pattern). When the cast is needed, use the pointer form so the result can be checked without exception overhead.
const_cast<T>(x)const_cast is the only cast that can add or remove const (or volatile) from a type. It can't change the underlying type at all. const_cast<int>(somePointer) is a compile error because it tries to change more than just the const-ness.
The honest motivation is interop. A function takes a char* because it was written before const existed, but the available data is const. The function is known not to modify the data. const_cast lets the call go through.
The cast strips const for the duration of the call. Reading through the resulting pointer is fine. Writing through it would be the problem.
What's wrong with this code?
This compiles. It might even run and print two different values for the same address, which looks like a bug in the compiler but isn't. maxCartItems was declared const, so modifying it through any path is undefined behavior. The compiler can assume maxCartItems never changes and substitute its value at the point of use, while still letting the pointer write happen. The two prints can disagree.
Fix: Don't declare the variable const if it needs to be modified. const_cast is meaningful only when removing const from a pointer or reference whose target object is not actually const.
This version is well-defined because the original object isn't const. The const was only on the pointer's type, and removing it doesn't lie about the object underneath.
The honest summary: const_cast rarely appears in new C++ code. When considering it, pause and ask whether the const can stay all the way through. If it's needed, document why.
reinterpret_cast<T>(x)reinterpret_cast tells the compiler to treat the bits of one type as if they were another type, with no conversion. It's the most dangerous cast and the one to use least often.
The legitimate uses are narrow and mostly involve low-level work: serialization, hardware register access, hash function tricks, and interoperating with C APIs that pass things around as opaque integers or void*. Almost everything else has a better answer.
Output (little-endian, x86 and most ARM):
The output looks reversed because most machines today store multi-byte integers least significant byte first. The cast didn't change the bytes, it let them be read one at a time. On a big-endian machine the same code would print ABCD. The platform dependence is why reinterpret_cast is dangerous: the same code can produce different results on different hardware.
Casting between unrelated pointer types is the other common use, usually when interoperating with a C API.
std::uintptr_t is an unsigned integer type guaranteed to be large enough to hold a pointer value. Round-tripping a pointer through std::uintptr_t and back is one of the few things reinterpret_cast guarantees works.
What it does not guarantee is that arbitrary pointer-to-pointer reinterpretations allow safe data access. Reading or writing through a pointer obtained by reinterpret_cast from an unrelated type is undefined behavior under the strict aliasing rules, with only a few documented exceptions (char*, unsigned char*, std::byte*). To inspect the bytes of an object, those character types are the safe path.
For most application code, reinterpret_cast is a smell. When it appears in a code review, the first question is "can this be done with static_cast instead?" The answer is usually yes.
For any cast, walk down this decision tree.
The diagram captures the common rule of thumb:
static_cast is the default. Most casts are this one.const_cast is for interop with code that's missing const annotations.dynamic_cast is for safe downcasting when the base class is polymorphic.reinterpret_cast is the last resort, and seeing one should prompt a question.A useful side-effect of using named casts is that each one is rare enough to grep for. A codebase with hundreds of static_casts and zero reinterpret_casts is in a different place from one with the reverse.
All four casts in one program, each doing its proper job.
Each cast is doing exactly one job. None of them could be swapped for another without a compile error or a worse failure mode. That's the pitch of named casts.
10 quizzes