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

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.


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
  • Module 06 requirement: type conversions must use a specific cast operator (C-style casts hide which cast you’re doing)

// 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

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: .0 suffix for whole float/double values

Expected output format:

char: 'a'
int: 97
float: 97.0f
double: 97.0

Think before coding:

  1. 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
  2. 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)
  3. 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)
  4. 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

Stage 1: Class structure (non-instantiable)

ScalarConverter.hpp
#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);
};
#endif

Stage 2: Type detection helpers

ScalarConverter.cpp - detection
#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

ScalarConverter.cpp - conversion
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

ScalarConverter.cpp - convert
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;
}

1. Wrong detection order:

// WRONG: "42" detected as double before int
if (isDouble(s)) ... // Matches "42"!
// RIGHT: Check int before double
if (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;

Test script:

Terminal window
# 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 decimals

Build a Serializer class that:

  • Converts a Data* pointer to uintptr_t (serialize)
  • Converts a uintptr_t back to Data* (deserialize)
  • Must be non-instantiable (static methods only)
  • You must create a non-empty Data struct

Key insight: This exercise teaches reinterpret_cast - the most dangerous cast that reinterprets the bit pattern.

Think before coding:

  1. 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 to unsigned long
  2. Why reinterpret_cast?

    • Pointer ↔ integer is not a type conversion
    • It’s a bit-level reinterpretation
    • static_cast won’t work for this
  3. What to test?

    • Round-trip: deserialize(serialize(ptr)) == ptr
    • Data integrity: values in struct unchanged
Data.hpp
#ifndef DATA_HPP
#define DATA_HPP
#include <string>
struct Data {
int id;
std::string name;
double value;
};
#endif
Serializer.hpp
#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
Serializer.cpp
#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);
}

Create a Base class with derived classes A, B, C. Implement:

  • Base* generate() - randomly returns new A, B, or C
  • void identify(Base* p) - prints actual type using pointer
  • void 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
Base.hpp
#ifndef BASE_HPP
#define BASE_HPP
class Base {
public:
virtual ~Base();
};
#endif
functions.cpp
#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;
}

// 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);

CastPurposeExercise
static_castSafe numeric conversionsEx00: char/int/float/double
reinterpret_castBit-level reinterpretationEx01: pointer ↔ integer
dynamic_castRuntime type identificationEx02: polymorphic downcasting
const_castRemove/add constNot covered (rarely needed)

When to use each:

NeedUse
Convert between numeric typesstatic_cast<target>(value)
Upcast (derived → base)Implicit or static_cast
Downcast (base → derived)dynamic_cast (safest) or static_cast (if certain)
Pointer ↔ integerreinterpret_cast
Remove constconst_cast (avoid if possible)

Continue your C++ journey:

Visit the Glossary for definitions of: