Module 06: C++ Casts
Key Concepts:
- static_cast
- dynamic_cast
- const_cast
- reinterpret_cast
- Type identification
- User-defined conversions
- The explicit keyword
Understanding Type Hierarchies
Section titled “Understanding Type Hierarchies”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 castConversion vs Reinterpretation
Section titled “Conversion vs Reinterpretation”Two fundamentally different operations:
- Conversion: Transform the value (e.g.,
3.14becomes3) - Reinterpretation: Keep the same bits, interpret differently (e.g., treat pointer as integer)
C++ casts distinguish these operations; C-style casts do not.
Understanding C++ Cast Operators
Section titled “Understanding C++ Cast Operators”This module introduces C++-style casts. Each cast has a specific purpose and is safer than C-style casts:
The static_cast<> Operator
Section titled “The static_cast<> Operator”int i = static_cast<int>(3.14); // Convert double to intBase* 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)
The dynamic_cast<> Operator
Section titled “The dynamic_cast<> Operator”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
NULLfor pointers if cast fails - Throws
std::bad_castexception 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)
The const_cast<> Operator
Section titled “The const_cast<> Operator”int* p = const_cast<int*>(const_ptr); // Remove constconst int* cp = const_cast<const int*>(p); // Add constconst_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
The reinterpret_cast<> Operator
Section titled “The reinterpret_cast<> Operator”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
The () C-Style Cast (AVOID!)
Section titled “The () C-Style Cast (AVOID!)”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
1. Why C++ Casts?
Section titled “1. Why C++ Casts?”C-Style Cast Problems
Section titled “C-Style Cast Problems”// C-style casts do everything - too powerful, no clarityint x = (int)3.14; // OKint* p = (int*)&x; // Dangerousconst int* cp = &x;int* mp = (int*)cp; // Removes const - dangerous!C++ Cast Benefits
Section titled “C++ Cast Benefits”- 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
2. static_cast
Section titled “2. static_cast”Purpose
Section titled “Purpose”Compile-time checked conversions between related types.
Use Cases
Section titled “Use Cases”1. Numeric conversions
double d = 3.14;int i = static_cast<int>(d); // 3float f = static_cast<float>(i); // 3.0f2. Enum to int (and vice versa)
enum Color { RED, GREEN, BLUE };Color c = static_cast<Color>(1); // GREENint n = static_cast<int>(c); // 13. 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);When NOT to Use
Section titled “When NOT to Use”// Unrelated types - won't compiledouble* dp;int* ip = static_cast<int*>(dp); // ERROR
// const removal - use const_castconst int* cp;int* p = static_cast<int*>(cp); // ERROR3. dynamic_cast
Section titled “3. dynamic_cast”Purpose
Section titled “Purpose”Runtime-checked downcasts in polymorphic class hierarchies.
Requirements
Section titled “Requirements”- Base class must have at least one virtual function
- Works with pointers and references
Why Virtual Functions Are Required (RTTI)
Section titled “Why Virtual Functions Are Required (RTTI)”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 RTTIclass 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);Pointer Syntax
Section titled “Pointer Syntax”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}Reference Syntax
Section titled “Reference Syntax”Base& br = derived_object;try { Derived& dr = dynamic_cast<Derived&>(br); // Cast succeeded}catch (std::bad_cast& e) { // Cast failed}Why Runtime Check?
Section titled “Why Runtime Check?”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}4. const_cast
Section titled “4. const_cast”Purpose
Section titled “Purpose”Add or remove const/volatile qualifiers.
Use Cases
Section titled “Use Cases”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);When It’s Safe
Section titled “When It’s Safe”// Original was non-const, temporarily made constint x = 42;const int* cp = &x;int* p = const_cast<int*>(cp); // Safe - x was never const*p = 100; // OKWhen It’s DANGEROUS
Section titled “When It’s DANGEROUS”const int CONSTANT = 42;int* p = const_cast<int*>(&CONSTANT);*p = 100; // UNDEFINED BEHAVIOR! CONSTANT is truly const5. reinterpret_cast
Section titled “5. reinterpret_cast”Purpose
Section titled “Purpose”Low-level reinterpretation of bit patterns. Most dangerous cast.
Use Cases
Section titled “Use Cases”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 dWarnings
Section titled “Warnings”- Highly platform-dependent
- Can violate aliasing rules
- Only use when you REALLY know what you’re doing
6. User-Defined Conversion Operators
Section titled “6. User-Defined Conversion Operators”Classes can define how they convert TO other types using conversion operators.
Syntax
Section titled “Syntax”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; }};
// UsageFraction half(1, 2);double d = half; // Calls operator double(), d = 0.5if (half) { // Calls operator bool() std::cout << "Non-zero fraction" << std::endl;}When to Use
Section titled “When to Use”- Converting your class to primitive types
- Enabling your class in boolean contexts (
if,while) - Interoperability with APIs expecting different types
7. The explicit Keyword
Section titled “7. The explicit Keyword”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!The Solution: explicit Keyword
Section titled “The Solution: explicit Keyword”class String {public: explicit String(int size) { // Mark constructor explicit _data = new char[size]; _size = size; }};
printString(42); // Error! No implicit conversionprintString(String(42)); // OK - explicit constructionWhen to Use explicit
Section titled “When to Use explicit”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};8. Cast Safety Spectrum
Section titled “8. Cast Safety Spectrum”C++ casts vary in how much they check:
| Cast | Checking Level | Safety |
|---|---|---|
dynamic_cast | Runtime | Safest |
static_cast | Compile-time | Moderate |
const_cast | Compile-time | Context-dependent |
reinterpret_cast | None | Most dangerous |
Choose the least permissive cast that works for your situation.
9. Module 06 Exercises
Section titled “9. Module 06 Exercises”ex00: Scalar Type Converter
Section titled “ex00: Scalar Type Converter”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"}String to Number Conversion: strtod
Section titled “String to Number Conversion: strtod”#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 errorsif (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 integerslong intValue = std::strtol(str, &endptr, 10); // base 10Special Value Detection: isnan, isinf
Section titled “Special Value Detection: isnan, isinf”#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 infinityif (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;}I/O Manipulators: fixed, setprecision
Section titled “I/O Manipulators: fixed, setprecision”#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 notationstd::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"Complete ScalarConverter Output Example
Section titled “Complete ScalarConverter Output Example”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;}ex01: Serialization
Section titled “ex01: Serialization”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;};
// Implementationuintptr_t Serializer::serialize(Data* ptr) { return reinterpret_cast<uintptr_t>(ptr);}
Data* Serializer::deserialize(uintptr_t raw) { return reinterpret_cast<Data*>(raw);}
// TestData original = {42, "test", 3.14f};uintptr_t serialized = Serializer::serialize(&original);Data* deserialized = Serializer::deserialize(serialized);// deserialized should == &originalex02: Type Identification
Section titled “ex02: Type Identification”class Base {public: virtual ~Base() {}};
class A : public Base { };class B : public Base { };class C : public Base { };
// Generate random A, B, or CBase* generate() { srand(time(NULL)); switch (rand() % 3) { case 0: return new A(); case 1: return new B(); case 2: return new C(); } return NULL;}Random Number Generation
Section titled “Random Number Generation”#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 numbersint random = std::rand(); // 0 to RAND_MAXint random0to9 = std::rand() % 10; // 0 to 9int random1to6 = std::rand() % 6 + 1; // 1 to 6 (dice roll)
// For RobotomyRequestForm - 50% success ratebool 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 (...) {}}10. Cast Selection Guide
Section titled “10. Cast Selection Guide”| Situation | Cast to Use |
|---|---|
| Numeric conversion (int <-> double) | static_cast |
| Upcasting (Derived* -> Base*) | static_cast |
| Downcasting (Base* -> Derived*) with virtual | dynamic_cast |
| Remove/add const | const_cast |
| Pointer <-> integer | reinterpret_cast |
| Unrelated pointer types | reinterpret_cast |
| Type-punning (reading bytes) | reinterpret_cast |
Quick Reference
Section titled “Quick Reference”// static_cast - compile-time, related typesint i = static_cast<int>(3.14);Derived* d = static_cast<Derived*>(base_ptr); // Unchecked!
// dynamic_cast - runtime, polymorphic downcastDerived* d = dynamic_cast<Derived*>(base_ptr); // NULL if failsDerived& r = dynamic_cast<Derived&>(base_ref); // throws if fails
// const_cast - add/remove constint* p = const_cast<int*>(const_ptr);
// reinterpret_cast - bit-level reinterpretationuintptr_t addr = reinterpret_cast<uintptr_t>(ptr);