Casting#
C-Style Cast (Legacy Casting)#
- Source:
C-style casts use the syntax (type)expression inherited from C. While
concise, they are dangerous because the compiler tries const_cast,
static_cast, and reinterpret_cast in sequence until one succeeds.
This makes it hard to know which conversion actually happens. The lack of
visual distinction also makes C-style casts difficult to search for in
large codebases. Modern C++ code should prefer explicit C++ casts for
clarity, safety, and maintainability.
#include <cmath>
#include <iostream>
int main(int argc, char *argv[]) {
double x = M_PI;
int xx = (int)x; // Truncates: 3.14159... -> 3
std::cout << xx << "\n";
long z = LONG_MAX;
int zz = (int)z; // Dangerous: overflow, undefined behavior
std::cout << zz << "\n";
}
Warning
C-style casts bypass type safety checks and can silently perform dangerous
conversions. Prefer static_cast for safe conversions and avoid C-style
casts in modern C++ code.
const_cast: Removing or Adding const#
- Source:
The const_cast operator adds or removes const (or volatile)
qualifiers from a pointer or reference. This is the only C++ cast that can
modify cv-qualifiers. The primary use case is interfacing with legacy C APIs
or older C++ code that lacks proper const-correctness. Modifying a truly
const object through const_cast results in undefined behavior, so it
should only be used when you are certain the underlying object was not
originally declared as const.
#include <iostream>
void legacy_api(int &x) {
const_cast<int &>(x) = 0; // Remove const to modify
}
int main(int argc, char *argv[]) {
int x = 123;
legacy_api(x);
std::cout << x << "\n"; // Output: 0
}
Note
Using const_cast to modify an object that was originally declared
const is undefined behavior. Only use it when you know the underlying
object is non-const.
reinterpret_cast: Low-Level Bit Reinterpretation#
- Source:
The reinterpret_cast operator performs low-level reinterpretation of
bit patterns between unrelated types. It is primarily used for converting
between pointer types, pointer-to-integer conversions, and accessing raw
memory representations. Common use cases include serialization, memory-mapped
I/O, and interfacing with hardware. This cast provides no type safety and
the results are implementation-defined, so it should be used sparingly and
only in systems programming contexts where such low-level access is necessary.
#include <iostream>
struct Point {
int x;
int y;
};
int main(int argc, char *argv[]) {
Point p{1, 2};
// View struct as raw bytes
char *buf = reinterpret_cast<char *>(&p);
for (size_t i = 0; i < sizeof(Point); ++i) {
std::cout << static_cast<int>(buf[i]) << " ";
}
// Output (little-endian): 1 0 0 0 2 0 0 0
}
static_cast: Compile-Time Type Conversion#
- Source:
The static_cast operator performs well-defined conversions between
related types at compile time. It handles numeric conversions (int to double),
enum-to-int conversions, void pointer conversions, and pointer upcasts in
class hierarchies. It can also invoke explicit constructors and conversion
operators. Unlike dynamic_cast, it performs no runtime type checking,
so downcasting with static_cast is unsafe unless you are certain of the
object’s actual type. When the conversion is valid, static_cast generates
efficient code with no runtime overhead.
#include <iostream>
#include <memory>
struct Base {
virtual ~Base() = default;
};
struct Derived : public Base {
int value = 42;
};
int main(int argc, char *argv[]) {
auto d = std::make_unique<Derived>();
// Safe upcast: Derived* -> Base*
Base *b = static_cast<Base *>(d.get());
// Unsafe downcast: only safe if b actually points to Derived
auto d2 = static_cast<Derived *>(b);
std::cout << d2->value << "\n"; // Output: 42
}
Warning
Using static_cast for downcasting is unsafe. If the object is not
actually of the target type, the behavior is undefined. Use dynamic_cast
when runtime type checking is needed.
dynamic_cast: Runtime Type-Safe Downcasting#
- Source:
The dynamic_cast operator performs runtime type checking for safe
downcasting in polymorphic class hierarchies. It requires the source type
to have at least one virtual function (typically the destructor) because
it relies on RTTI (Run-Time Type Information) stored in the virtual table.
For pointer casts, it returns nullptr on failure; for reference casts,
it throws std::bad_cast. This is the safest way to navigate class
hierarchies when the actual type is uncertain at compile time, such as when
processing heterogeneous collections of objects.
#include <iostream>
#include <memory>
struct Base {
virtual ~Base() = default;
};
struct Derived : public Base {
void greet() { std::cout << "Hello from Derived\n"; }
};
int main(int argc, char *argv[]) {
auto base = std::make_unique<Base>();
auto derived = std::make_unique<Derived>();
// Failed downcast: base doesn't point to Derived
auto d1 = dynamic_cast<Derived *>(base.get());
std::cout << "base -> Derived: " << (d1 ? "success" : "failed") << "\n";
// Successful upcast: Derived* -> Base*
auto b2 = dynamic_cast<Base *>(derived.get());
std::cout << "Derived -> Base: " << (b2 ? "success" : "failed") << "\n";
}
Note
dynamic_cast has runtime overhead due to RTTI (Run-Time Type Information).
In performance-critical code where types are known, prefer static_cast.