Module 06: C++ Casts
Key Concepts:
- static_cast
- dynamic_cast
- const_cast
- reinterpret_cast
- Type identification
- User-defined conversions
- The explicit keyword
Why This Matters
Section titled “Why This Matters”C++ provides four explicit cast operators to replace C-style casts. Each has a specific purpose, making your intentions clear and catching errors at compile time.
Use the right cast for the job—it documents your intent and helps prevent bugs.
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
- Module 06 requirement: type conversions must use a specific cast operator (C-style casts hide which cast you’re doing)
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 |
Exercise 00: Conversion of Scalar Types
Section titled “Exercise 00: Conversion of Scalar Types”Subject Analysis
Section titled “Subject Analysis”Build a ScalarConverter class with a static method that:
- Takes a string literal representing a C++ literal value
- Detects what type the literal is (char, int, float, double)
- Converts and displays the value in all four scalar types
- Handles special values:
nan,nanf,inf,inff,+inf,-inf, etc.
Key constraints:
- Class must be non-instantiable (only static method)
- Must handle edge cases: overflow, non-displayable chars
- Display format:
.0suffix for whole float/double values
Expected output format:
char: 'a'int: 97float: 97.0fdouble: 97.0Approach Strategy
Section titled “Approach Strategy”Think before coding:
-
What types of input can we receive?
- Single character:
'a','*' - Integer:
42,-17,0 - Float:
42.0f,-4.2f,nanf,inff - Double:
42.0,-4.2,nan,inf
- Single character:
-
How to detect the type?
- char: single non-digit character
- int: all digits with optional leading sign
- float: has ‘f’ suffix (or special: nanf, inff)
- double: has decimal point without ‘f’ (or special: nan, inf)
-
Order of detection matters:
1. Special values first (nan, inf variants)2. Single char (but not a digit)3. Integer (all digits)4. Float (ends with 'f')5. Double (has decimal) -
What can go wrong in conversion?
- int overflow → “impossible”
- char out of range (0-127) → “impossible”
- non-printable char → “Non displayable”
- inf/nan → some conversions impossible
Progressive Code Building
Section titled “Progressive Code Building”Stage 1: Class structure (non-instantiable)
#ifndef SCALARCONVERTER_HPP#define SCALARCONVERTER_HPP
#include <string>
class ScalarConverter {private: // Private constructor = cannot instantiate ScalarConverter(); ScalarConverter(const ScalarConverter& other); ScalarConverter& operator=(const ScalarConverter& other); ~ScalarConverter();
public: static void convert(const std::string& literal);};
#endifStage 2: Type detection helpers
#include "ScalarConverter.hpp"#include <cstdlib> // strtod, strtol#include <cctype> // isdigit, isprint#include <climits> // INT_MIN, INT_MAX#include <cmath> // isnan, isinf#include <iostream>#include <iomanip> // setprecision
// Private helpers (in anonymous namespace or as private static)static bool isSpecialFloat(const std::string& s) { return s == "nanf" || s == "inff" || s == "+inff" || s == "-inff";}
static bool isSpecialDouble(const std::string& s) { return s == "nan" || s == "inf" || s == "+inf" || s == "-inf";}
static bool isChar(const std::string& s) { // Single character that is NOT a digit return s.length() == 1 && !std::isdigit(s[0]);}
static bool isInt(const std::string& s) { if (s.empty()) return false; size_t start = 0; if (s[0] == '+' || s[0] == '-') start = 1; if (start >= s.length()) return false;
for (size_t i = start; i < s.length(); i++) { if (!std::isdigit(s[i])) return false; } return true;}
static bool isFloat(const std::string& s) { if (s.empty() || s[s.length() - 1] != 'f') return false; std::string withoutF = s.substr(0, s.length() - 1); char* end; std::strtod(withoutF.c_str(), &end); return *end == '\0' && withoutF.find('.') != std::string::npos;}
static bool isDouble(const std::string& s) { if (s.empty()) return false; char* end; std::strtod(s.c_str(), &end); return *end == '\0' && s.find('.') != std::string::npos;}Stage 3: Conversion and display
static void printChar(double d) { if (std::isnan(d) || std::isinf(d) || d < 0 || d > 127) std::cout << "char: impossible" << std::endl; else if (!std::isprint(static_cast<int>(d))) std::cout << "char: Non displayable" << std::endl; else std::cout << "char: '" << static_cast<char>(d) << "'" << std::endl;}
static void printInt(double d) { if (std::isnan(d) || std::isinf(d) || d < INT_MIN || d > INT_MAX) std::cout << "int: impossible" << std::endl; else std::cout << "int: " << static_cast<int>(d) << std::endl;}
static void printFloatDouble(double d) { std::cout << std::fixed << std::setprecision(1); std::cout << "float: " << static_cast<float>(d) << "f" << std::endl; std::cout << "double: " << d << std::endl;}
static void fromDouble(double d) { printChar(d); printInt(d); printFloatDouble(d);}Stage 4: Main convert function
void ScalarConverter::convert(const std::string& literal) { // Handle special float values if (isSpecialFloat(literal)) { std::string withoutF = literal.substr(0, literal.length() - 1); double d = std::strtod(withoutF.c_str(), NULL); fromDouble(d); return; }
// Handle special double values if (isSpecialDouble(literal)) { double d = std::strtod(literal.c_str(), NULL); fromDouble(d); return; }
// Handle char if (isChar(literal)) { fromDouble(static_cast<double>(literal[0])); return; }
// Handle int if (isInt(literal)) { long l = std::strtol(literal.c_str(), NULL, 10); if (l < INT_MIN || l > INT_MAX) { std::cout << "char: impossible" << std::endl; std::cout << "int: impossible" << std::endl; std::cout << "float: impossible" << std::endl; std::cout << "double: impossible" << std::endl; return; } fromDouble(static_cast<double>(l)); return; }
// Handle float if (isFloat(literal)) { std::string withoutF = literal.substr(0, literal.length() - 1); double d = std::strtod(withoutF.c_str(), NULL); fromDouble(d); return; }
// Handle double if (isDouble(literal)) { double d = std::strtod(literal.c_str(), NULL); fromDouble(d); return; }
// Invalid input std::cout << "char: impossible" << std::endl; std::cout << "int: impossible" << std::endl; std::cout << "float: impossible" << std::endl; std::cout << "double: impossible" << std::endl;}Common Pitfalls
Section titled “Common Pitfalls”1. Wrong detection order:
// WRONG: "42" detected as double before intif (isDouble(s)) ... // Matches "42"!
// RIGHT: Check int before doubleif (isInt(s)) ...if (isDouble(s)) ...2. Missing precision format:
// WRONG: Outputs "42f" and "42"std::cout << "float: " << 42.0f << "f" << std::endl;
// RIGHT: Outputs "42.0f" and "42.0"std::cout << std::fixed << std::setprecision(1);std::cout << "float: " << 42.0f << "f" << std::endl;Testing Tips
Section titled “Testing Tips”Test script:
# Basic types./convert 'a' # char./convert 42 # int./convert 42.0f # float./convert 42.0 # double
# Edge cases./convert 0 # Zero./convert -42 # Negative./convert 127 # Max char./convert 128 # Char impossible
# Special values./convert nan./convert nanf./convert inf./convert inff./convert +inf./convert -inff
# Non-displayable./convert 0 # Non displayable (null char)./convert 31 # Non displayable (control char)
# Invalid./convert "" # Empty./convert "hello" # Invalid literal./convert "42.42.42" # Multiple decimalsExercise 01: Serialization
Section titled “Exercise 01: Serialization”Subject Analysis
Section titled “Subject Analysis”Build a Serializer class that:
- Converts a
Data*pointer touintptr_t(serialize) - Converts a
uintptr_tback toData*(deserialize) - Must be non-instantiable (static methods only)
- You must create a non-empty
Datastruct
Key insight: This exercise teaches reinterpret_cast - the most dangerous cast that reinterprets the bit pattern.
Approach Strategy
Section titled “Approach Strategy”Think before coding:
-
What is uintptr_t?
- An unsigned integer type guaranteed to hold any pointer
- Defined in
<cstdint>(C++11) or<stdint.h>(C99) - For C++98: use
<stdint.h>or cast tounsigned long
-
Why reinterpret_cast?
- Pointer ↔ integer is not a type conversion
- It’s a bit-level reinterpretation
static_castwon’t work for this
-
What to test?
- Round-trip:
deserialize(serialize(ptr)) == ptr - Data integrity: values in struct unchanged
- Round-trip:
Final Code
Section titled “Final Code”#ifndef DATA_HPP#define DATA_HPP
#include <string>
struct Data { int id; std::string name; double value;};
#endif#ifndef SERIALIZER_HPP#define SERIALIZER_HPP
#include <stdint.h>#include "Data.hpp"
class Serializer {private: Serializer(); Serializer(const Serializer& other); Serializer& operator=(const Serializer& other); ~Serializer();
public: static uintptr_t serialize(Data* ptr); static Data* deserialize(uintptr_t raw);};
#endif#include "Serializer.hpp"
Serializer::Serializer() {}Serializer::Serializer(const Serializer& other) { (void)other; }Serializer& Serializer::operator=(const Serializer& other) { (void)other; return *this; }Serializer::~Serializer() {}
uintptr_t Serializer::serialize(Data* ptr) { return reinterpret_cast<uintptr_t>(ptr);}
Data* Serializer::deserialize(uintptr_t raw) { return reinterpret_cast<Data*>(raw);}Exercise 02: Identify Real Type
Section titled “Exercise 02: Identify Real Type”Subject Analysis
Section titled “Subject Analysis”Create a Base class with derived classes A, B, C. Implement:
Base* generate()- randomly returns new A, B, or Cvoid identify(Base* p)- prints actual type using pointervoid identify(Base& p)- prints actual type using reference
Key constraints:
<typeinfo>header is FORBIDDEN- Reference version cannot use pointers internally
- Base must have a virtual destructor
- Base, A, B, C do not have to follow Orthodox Canonical Form for this exercise
Final Code
Section titled “Final Code”#ifndef BASE_HPP#define BASE_HPP
class Base {public: virtual ~Base();};
#endif#include "Base.hpp"#include "A.hpp"#include "B.hpp"#include "C.hpp"#include <cstdlib>#include <ctime>#include <iostream>
Base* generate() { static bool seeded = false; if (!seeded) { std::srand(std::time(NULL)); seeded = true; } switch (std::rand() % 3) { case 0: return new A(); case 1: return new B(); case 2: return new C(); } return NULL;}
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; else std::cout << "Unknown" << std::endl;}
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; return; } catch (...) {} std::cout << "Unknown" << std::endl;}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);Module 06 Summary: The Four C++ Casts
Section titled “Module 06 Summary: The Four C++ Casts”| Cast | Purpose | Exercise |
|---|---|---|
static_cast | Safe numeric conversions | Ex00: char/int/float/double |
reinterpret_cast | Bit-level reinterpretation | Ex01: pointer ↔ integer |
dynamic_cast | Runtime type identification | Ex02: polymorphic downcasting |
const_cast | Remove/add const | Not covered (rarely needed) |
When to use each:
| Need | Use |
|---|---|
| Convert between numeric types | static_cast<target>(value) |
| Upcast (derived → base) | Implicit or static_cast |
| Downcast (base → derived) | dynamic_cast (safest) or static_cast (if certain) |
| Pointer ↔ integer | reinterpret_cast |
| Remove const | const_cast (avoid if possible) |
Related Concepts
Section titled “Related Concepts”Continue your C++ journey:
- Previous: Module 05: Exceptions - Review error handling
- Next: Module 07: Templates - Learn generic programming
Key Terms from This Module
Section titled “Key Terms from This Module”Visit the Glossary for definitions of: