Skip to content

Module 06: C++ Casts

Download Official Subject PDF

Key Concepts:

  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast
  • Type identification
  • User-defined conversions
  • The explicit keyword

Before diving into casts, understand that types form hierarchies based on precision and generality:

More Specific (narrow) More General (wide)
int ────────> double
Derived* ────────> Base*
const int* ────────> int* (removes restriction)
int* ────────> void*

Key insight: Conversions from specific to general are usually safe and implicit. Conversions from general to specific require explicit casts because information can be lost.

int i = 42;
double d = i; // Implicit: int -> double (safe, no loss)
int j = d; // Warning! double -> int loses decimal part
Derived* dp = new Derived();
Base* bp = dp; // Implicit: Derived* -> Base* (safe, IS-A relationship)
Derived* dp2 = bp; // Error! Base* -> Derived* needs explicit cast

Two fundamentally different operations:

  • Conversion: Transform the value (e.g., 3.14 becomes 3)
  • Reinterpretation: Keep the same bits, interpret differently (e.g., treat pointer as integer)

C++ casts distinguish these operations; C-style casts do not.


This module introduces C++-style casts. Each cast has a specific purpose and is safer than C-style casts:

int i = static_cast<int>(3.14); // Convert double to int
Base* bp = static_cast<Base*>(derived); // Upcast (safe)
Derived* dp = static_cast<Derived*>(bp); // Downcast (unchecked!)
  • static_cast<> performs compile-time type conversions
  • It checks at compile time that the conversion makes sense (mostly)
  • Safe for: numeric conversions, upcasts, void* conversions
  • Dangerous for: downcasts (no runtime check - assumes you know it’s safe)
  • It’s “static” because it checks types at compile time (static type checking)
Derived* dp = dynamic_cast<Derived*>(basePtr);
if (dp != NULL) {
// Cast succeeded - basePtr actually points to Derived!
}
  • dynamic_cast<> performs runtime type conversions
  • It checks at runtime if the cast is actually valid
  • Returns NULL for pointers if cast fails
  • Throws std::bad_cast exception for references if cast fails
  • Only works with polymorphic classes (must have at least one virtual function)
  • It’s “dynamic” because it checks types at runtime (dynamic type checking)
int* p = const_cast<int*>(const_ptr); // Remove const
const int* cp = const_cast<const int*>(p); // Add const
  • const_cast<> only changes const/volatile qualifiers
  • It cannot change the underlying type (int remains int)
  • Removing const from truly const data leads to undefined behavior!
  • Only safe to use when the original data wasn’t truly const
  • Mainly used for compatibility with legacy APIs that aren’t const-correct
uintptr_t addr = reinterpret_cast<uintptr_t>(ptr);
char* bytes = reinterpret_cast<char*>(&data);
  • reinterpret_cast<> performs low-level bit reinterpretation
  • It tells the compiler “trust me, treat this memory as a different type”
  • Most dangerous cast - it doesn’t check anything, just reinterprets bits
  • Use for: pointer-to-integer, integer-to-pointer, unrelated pointer types
  • Results are platform-dependent and can violate type safety
  • Only use when you REALLY know what you’re doing
int x = (int)3.14; // C-style - does ANY kind of cast
  • C-style casts can do anything: static_cast, const_cast, reinterpret_cast
  • Too powerful - hard to see what’s actually happening
  • Not searchable - can’t grep for all casts easily
  • 42 Rule: Use C++ casts only, never C-style casts

// C-style casts do everything - too powerful, no clarity
int x = (int)3.14; // OK
int* p = (int*)&x; // Dangerous
const int* cp = &x;
int* mp = (int*)cp; // Removes const - dangerous!
  • Explicit intent: Clear what type of conversion
  • Searchable: Easy to grep for casts
  • Type-safe: Compile-time/runtime checks
  • Restrictive: Each cast only does specific conversions

Compile-time checked conversions between related types.

1. Numeric conversions

double d = 3.14;
int i = static_cast<int>(d); // 3
float f = static_cast<float>(i); // 3.0f

2. Enum to int (and vice versa)

enum Color { RED, GREEN, BLUE };
Color c = static_cast<Color>(1); // GREEN
int n = static_cast<int>(c); // 1

3. Pointer up/downcasts (without polymorphism)

class Base { };
class Derived : public Base { };
Derived d;
Base* bp = static_cast<Base*>(&d); // Upcast (safe)
Derived* dp = static_cast<Derived*>(bp); // Downcast (unchecked!)

4. void* conversions

int x = 42;
void* vp = static_cast<void*>(&x);
int* ip = static_cast<int*>(vp);
// Unrelated types - won't compile
double* dp;
int* ip = static_cast<int*>(dp); // ERROR
// const removal - use const_cast
const int* cp;
int* p = static_cast<int*>(cp); // ERROR

Runtime-checked downcasts in polymorphic class hierarchies.

  • Base class must have at least one virtual function
  • Works with pointers and references

dynamic_cast uses RTTI (Runtime Type Information) to check types at runtime. RTTI is only stored for polymorphic classes (classes with virtual functions).

When a class has virtual functions, the compiler adds a hidden pointer (vptr) to each object that points to a vtable. The vtable contains not just function pointers but also type information that dynamic_cast uses.

class NotPolymorphic { }; // No RTTI
class Polymorphic { virtual ~Polymorphic() {} }; // Has RTTI
// This fails at compile time - no RTTI available:
// NotPolymorphic* np = dynamic_cast<NotPolymorphic*>(ptr);
// This works - RTTI available:
Polymorphic* pp = dynamic_cast<Polymorphic*>(ptr);
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base { };
Base* bp = new Derived();
Derived* dp = dynamic_cast<Derived*>(bp);
if (dp != NULL) {
// Cast succeeded - bp actually pointed to Derived
}
else {
// Cast failed - bp pointed to Base or other type
}
Base& br = derived_object;
try {
Derived& dr = dynamic_cast<Derived&>(br);
// Cast succeeded
}
catch (std::bad_cast& e) {
// Cast failed
}
class Animal { public: virtual ~Animal() {} };
class Dog : public Animal { };
class Cat : public Animal { };
Animal* animal = getAnimalFromUser(); // Could be Dog or Cat
Dog* dog = dynamic_cast<Dog*>(animal);
if (dog) {
dog->bark(); // Safe - we know it's really a Dog
}

Add or remove const/volatile qualifiers.

Remove const (use carefully!)

void legacyFunction(char* str); // Can't change this old function
const char* message = "Hello";
legacyFunction(const_cast<char*>(message)); // Remove const
// WARNING: Modifying truly const data is UNDEFINED BEHAVIOR!

Add const

int x = 42;
const int* cp = const_cast<const int*>(&x);
// Original was non-const, temporarily made const
int x = 42;
const int* cp = &x;
int* p = const_cast<int*>(cp); // Safe - x was never const
*p = 100; // OK
const int CONSTANT = 42;
int* p = const_cast<int*>(&CONSTANT);
*p = 100; // UNDEFINED BEHAVIOR! CONSTANT is truly const

Low-level reinterpretation of bit patterns. Most dangerous cast.

Pointer to integer (and back)

int x = 42;
uintptr_t address = reinterpret_cast<uintptr_t>(&x);
int* p = reinterpret_cast<int*>(address);

Pointer to different type

struct Data { int x; float y; };
Data d = {42, 3.14f};
char* bytes = reinterpret_cast<char*>(&d);
// Now can access raw bytes of d
  • Highly platform-dependent
  • Can violate aliasing rules
  • Only use when you REALLY know what you’re doing

Classes can define how they convert TO other types using conversion operators.

class Fraction {
private:
int _numerator;
int _denominator;
public:
Fraction(int num, int den) : _numerator(num), _denominator(den) {}
// Conversion operator: Fraction -> double
operator double() const {
return static_cast<double>(_numerator) / _denominator;
}
// Conversion operator: Fraction -> bool (is it non-zero?)
operator bool() const {
return _numerator != 0;
}
};
// Usage
Fraction half(1, 2);
double d = half; // Calls operator double(), d = 0.5
if (half) { // Calls operator bool()
std::cout << "Non-zero fraction" << std::endl;
}
  • Converting your class to primitive types
  • Enabling your class in boolean contexts (if, while)
  • Interoperability with APIs expecting different types

Constructors can act as implicit conversion operators. The explicit keyword prevents this.

The Problem: Implicit Constructor Conversion

Section titled “The Problem: Implicit Constructor Conversion”
class String {
public:
String(int size) { // Allocate string of given size
_data = new char[size];
_size = size;
}
// ...
};
void printString(const String& s);
// Accidental implicit conversion!
printString(42); // Creates String(42) - probably not what you wanted!
class String {
public:
explicit String(int size) { // Mark constructor explicit
_data = new char[size];
_size = size;
}
};
printString(42); // Error! No implicit conversion
printString(String(42)); // OK - explicit construction

Use explicit on single-argument constructors unless you specifically want implicit conversion:

class Vector3 {
public:
explicit Vector3(float all); // explicit: Vector3(5.0f) not from bare 5.0f
Vector3(float x, float y, float z); // Multiple args - can't be implicit anyway
};
class Wrapper {
public:
Wrapper(const std::string& s); // Maybe implicit is OK here
explicit Wrapper(int fd); // But not from bare int
};

C++ casts vary in how much they check:

CastChecking LevelSafety
dynamic_castRuntimeSafest
static_castCompile-timeModerate
const_castCompile-timeContext-dependent
reinterpret_castNoneMost dangerous

Choose the least permissive cast that works for your situation.


class ScalarConverter {
private:
ScalarConverter(); // Not instantiable
public:
static void convert(const std::string& literal);
};
// Detect type and convert to all scalar types
// Input: "42", "42.0f", "42.0", "'*'", "nan", etc.
// Output: char, int, float, double representations
void ScalarConverter::convert(const std::string& literal) {
// 1. Detect input type (char, int, float, double)
// 2. Parse the value
// 3. Convert and display all four types
// Handle special cases: nan, inf, impossible conversions
}

Type Detection Logic:

bool isChar(const std::string& s) {
return s.length() == 1 && !isdigit(s[0]);
}
bool isInt(const std::string& s) {
// Check if all digits (with optional leading sign)
}
bool isFloat(const std::string& s) {
// Check for 'f' suffix, decimal point
// Handle "nanf", "-inff", "+inff"
}
bool isDouble(const std::string& s) {
// Has decimal point, no 'f' suffix
// Handle "nan", "-inf", "+inf"
}
#include <cstdlib> // for strtod, strtol
// strtod - string to double (safer than atof)
const char* str = "3.14159";
char* endptr;
double value = std::strtod(str, &endptr);
// Check for conversion errors
if (endptr == str) {
// No conversion performed
std::cout << "Invalid number" << std::endl;
}
else if (*endptr != '\0') {
// Partial conversion (trailing characters)
std::cout << "Trailing chars: " << endptr << std::endl;
}
// Also useful: strtol for integers
long intValue = std::strtol(str, &endptr, 10); // base 10
#include <cmath> // for isnan, isinf
double value = std::strtod(str, NULL);
// Check for NaN (Not a Number)
if (std::isnan(value)) {
std::cout << "Value is nan" << std::endl;
}
// Check for infinity
if (std::isinf(value)) {
if (value > 0)
std::cout << "Value is +inf" << std::endl;
else
std::cout << "Value is -inf" << std::endl;
}

Character Classification: isprint, isdigit

Section titled “Character Classification: isprint, isdigit”
#include <cctype>
char c = '*';
// isprint - is it a printable character?
if (std::isprint(c)) {
std::cout << "char: '" << c << "'" << std::endl;
}
else {
std::cout << "char: Non displayable" << std::endl;
}
// isdigit - is it a digit?
if (std::isdigit(c)) {
std::cout << c << " is a digit" << std::endl;
}
#include <iomanip> // for setprecision
#include <iostream>
double d = 42.0;
float f = 42.0f;
// Default output might show "42" instead of "42.0"
// Use fixed and setprecision to control decimal places
std::cout << std::fixed; // Use fixed-point notation
std::cout << std::setprecision(1); // 1 decimal place
std::cout << "float: " << f << "f" << std::endl; // "42.0f"
std::cout << "double: " << d << std::endl; // "42.0"
// For more precision:
std::cout << std::setprecision(6);
std::cout << 3.14159265358979 << std::endl; // "3.141593"
void displayConversions(double value) {
// char
if (std::isnan(value) || std::isinf(value) ||
value < 0 || value > 127) {
std::cout << "char: impossible" << std::endl;
}
else if (!std::isprint(static_cast<char>(value))) {
std::cout << "char: Non displayable" << std::endl;
}
else {
std::cout << "char: '" << static_cast<char>(value) << "'" << std::endl;
}
// int
if (std::isnan(value) || std::isinf(value) ||
value < INT_MIN || value > INT_MAX) {
std::cout << "int: impossible" << std::endl;
}
else {
std::cout << "int: " << static_cast<int>(value) << std::endl;
}
// float
std::cout << std::fixed << std::setprecision(1);
if (std::isnan(value))
std::cout << "float: nanf" << std::endl;
else if (std::isinf(value))
std::cout << "float: " << (value > 0 ? "+inff" : "-inff") << std::endl;
else
std::cout << "float: " << static_cast<float>(value) << "f" << std::endl;
// double
if (std::isnan(value))
std::cout << "double: nan" << std::endl;
else if (std::isinf(value))
std::cout << "double: " << (value > 0 ? "+inf" : "-inf") << std::endl;
else
std::cout << "double: " << value << std::endl;
}
class Serializer {
private:
Serializer(); // Not instantiable
public:
static uintptr_t serialize(Data* ptr);
static Data* deserialize(uintptr_t raw);
};
struct Data {
int id;
std::string name;
float value;
};
// Implementation
uintptr_t Serializer::serialize(Data* ptr) {
return reinterpret_cast<uintptr_t>(ptr);
}
Data* Serializer::deserialize(uintptr_t raw) {
return reinterpret_cast<Data*>(raw);
}
// Test
Data original = {42, "test", 3.14f};
uintptr_t serialized = Serializer::serialize(&original);
Data* deserialized = Serializer::deserialize(serialized);
// deserialized should == &original
class Base {
public:
virtual ~Base() {}
};
class A : public Base { };
class B : public Base { };
class C : public Base { };
// Generate random A, B, or C
Base* generate() {
srand(time(NULL));
switch (rand() % 3) {
case 0: return new A();
case 1: return new B();
case 2: return new C();
}
return NULL;
}
#include <cstdlib> // for srand, rand
#include <ctime> // for time
// Seed the random number generator (do once at program start)
std::srand(std::time(NULL)); // Use current time as seed
// Generate random numbers
int random = std::rand(); // 0 to RAND_MAX
int random0to9 = std::rand() % 10; // 0 to 9
int random1to6 = std::rand() % 6 + 1; // 1 to 6 (dice roll)
// For RobotomyRequestForm - 50% success rate
bool success = (std::rand() % 2 == 0);
if (success) {
std::cout << _target << " has been robotomized successfully" << std::endl;
} else {
std::cout << "Robotomy of " << _target << " failed" << std::endl;
}
// Identify using pointer (returns NULL on failure)
void identify(Base* p) {
if (dynamic_cast<A*>(p))
std::cout << "A" << std::endl;
else if (dynamic_cast<B*>(p))
std::cout << "B" << std::endl;
else if (dynamic_cast<C*>(p))
std::cout << "C" << std::endl;
}
// Identify using reference (throws on failure)
void identify(Base& p) {
try {
(void)dynamic_cast<A&>(p);
std::cout << "A" << std::endl;
return;
} catch (...) {}
try {
(void)dynamic_cast<B&>(p);
std::cout << "B" << std::endl;
return;
} catch (...) {}
try {
(void)dynamic_cast<C&>(p);
std::cout << "C" << std::endl;
} catch (...) {}
}

SituationCast to Use
Numeric conversion (int <-> double)static_cast
Upcasting (Derived* -> Base*)static_cast
Downcasting (Base* -> Derived*) with virtualdynamic_cast
Remove/add constconst_cast
Pointer <-> integerreinterpret_cast
Unrelated pointer typesreinterpret_cast
Type-punning (reading bytes)reinterpret_cast

// static_cast - compile-time, related types
int i = static_cast<int>(3.14);
Derived* d = static_cast<Derived*>(base_ptr); // Unchecked!
// dynamic_cast - runtime, polymorphic downcast
Derived* d = dynamic_cast<Derived*>(base_ptr); // NULL if fails
Derived& r = dynamic_cast<Derived&>(base_ref); // throws if fails
// const_cast - add/remove const
int* p = const_cast<int*>(const_ptr);
// reinterpret_cast - bit-level reinterpretation
uintptr_t addr = reinterpret_cast<uintptr_t>(ptr);