Skip to content

Module 06 Tutorial

Prerequisites: Review Module 06 Concepts first.

This module introduces C++ type casting operators. You’ll learn when to use each cast type through practical exercises: converting scalar types, serializing pointers, and identifying types at runtime.


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;
}

Type detection logic:

FunctionLogicWhy This Way
isChar()Length 1, not a digitDigit chars like ‘5’ would be detected as int
isInt()All digits, optional signMust reject strings with decimal points
isFloat()Ends with ‘f’, valid number, has ’.’Distinguishes from int and double
isDouble()Valid number, has ’.’, no ‘f’ suffixFallback numeric type

Conversion strategy:

From TypeStrategy
charCast to double, then to all types
intCast to double first for range checking
float/doubleParse with strtod, display all
specialParse directly (nan/inf handling built-in)

Why cast everything to double first?

// double has enough precision to hold all other types
// Makes range checking uniform
double d = /* input */;
printChar(d); // Check 0-127 range
printInt(d); // Check INT_MIN-INT_MAX
printFloatDouble(d); // Always works

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;

3. Forgetting edge cases:

// WRONG: Crashes on empty string
if (s[0] == '-') ...
// RIGHT: Check length first
if (!s.empty() && s[0] == '-') ...

4. Not handling sign-only strings:

// "-" and "+" are not valid numbers
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; // Just sign, no digits
// ...
}

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

Expected outputs:

Terminal window
$ ./convert 'a'
char: 'a'
int: 97
float: 97.0f
double: 97.0
$ ./convert nan
char: impossible
int: impossible
float: nanf
double: nan
$ ./convert 128
char: impossible
int: 128
float: 128.0f
double: 128.0
ScalarConverter.hpp
#ifndef SCALARCONVERTER_HPP
#define SCALARCONVERTER_HPP
#include <string>
class ScalarConverter {
private:
ScalarConverter();
ScalarConverter(const ScalarConverter& other);
ScalarConverter& operator=(const ScalarConverter& other);
~ScalarConverter();
public:
static void convert(const std::string& literal);
};
#endif
ScalarConverter.cpp
#include "ScalarConverter.hpp"
#include <cstdlib>
#include <cctype>
#include <climits>
#include <cmath>
#include <iostream>
#include <iomanip>
// OCF - never used but required for non-instantiable class
ScalarConverter::ScalarConverter() {}
ScalarConverter::ScalarConverter(const ScalarConverter& other) { (void)other; }
ScalarConverter& ScalarConverter::operator=(const ScalarConverter& other) { (void)other; return *this; }
ScalarConverter::~ScalarConverter() {}
// Type detection helpers
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) {
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;
}
// Conversion helpers
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);
}
void ScalarConverter::convert(const std::string& literal) {
if (isSpecialFloat(literal)) {
std::string withoutF = literal.substr(0, literal.length() - 1);
fromDouble(std::strtod(withoutF.c_str(), NULL));
return;
}
if (isSpecialDouble(literal)) {
fromDouble(std::strtod(literal.c_str(), NULL));
return;
}
if (isChar(literal)) {
fromDouble(static_cast<double>(literal[0]));
return;
}
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;
}
if (isFloat(literal)) {
std::string withoutF = literal.substr(0, literal.length() - 1);
fromDouble(std::strtod(withoutF.c_str(), NULL));
return;
}
if (isDouble(literal)) {
fromDouble(std::strtod(literal.c_str(), NULL));
return;
}
std::cout << "char: impossible" << std::endl;
std::cout << "int: impossible" << std::endl;
std::cout << "float: impossible" << std::endl;
std::cout << "double: impossible" << std::endl;
}
main.cpp
#include "ScalarConverter.hpp"
#include <iostream>
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <literal>" << std::endl;
return 1;
}
ScalarConverter::convert(argv[1]);
return 0;
}

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

Stage 1: Data structure

Data.hpp
#ifndef DATA_HPP
#define DATA_HPP
#include <string>
struct Data {
int id;
std::string name;
double value;
};
#endif

Stage 2: Serializer class

Serializer.hpp
#ifndef SERIALIZER_HPP
#define SERIALIZER_HPP
#include <stdint.h> // uintptr_t (C99/C++98 compatible)
#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

Stage 3: Implementation

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

The core operations:

OperationCodeWhat Happens
Serializereinterpret_cast<uintptr_t>(ptr)Takes the memory address bits, treats them as an integer
Deserializereinterpret_cast<Data*>(raw)Takes integer bits, treats them as a memory address

Why reinterpret_cast is required:

// static_cast CANNOT do this - compilation error:
uintptr_t bad = static_cast<uintptr_t>(ptr); // ERROR!
// reinterpret_cast is the ONLY cast that can:
uintptr_t good = reinterpret_cast<uintptr_t>(ptr); // OK

Memory visualization:

Data object at address 0x7fff5fbff8a0:
┌─────────────────────────────┐
│ id: 42 │
│ name: "test" │
│ value: 3.14 │
└─────────────────────────────┘
serialize(&data) → 0x7fff5fbff8a0 (as integer)
deserialize(0x7fff5fbff8a0) → pointer to same object

1. Empty Data struct:

// WRONG: Subject requires non-empty Data
struct Data {};
// RIGHT: Add some members
struct Data {
int value;
std::string str;
};

2. Not testing round-trip:

// WRONG: Only testing one direction
uintptr_t raw = Serializer::serialize(&d);
std::cout << raw << std::endl; // Proves nothing!
// RIGHT: Complete round-trip verification
Data* result = Serializer::deserialize(Serializer::serialize(&d));
if (result == &d)
std::cout << "Success: same address!" << std::endl;

3. Using C++11 header:

// May not work in strict C++98
#include <cstdint>
// Use C99 header instead (works in C++98)
#include <stdint.h>
main.cpp
#include "Serializer.hpp"
#include <iostream>
int main() {
// Create and populate Data
Data original;
original.id = 42;
original.name = "test_data";
original.value = 3.14159;
std::cout << "Original address: " << &original << std::endl;
std::cout << "Original values: id=" << original.id
<< ", name=" << original.name
<< ", value=" << original.value << std::endl;
// Serialize
uintptr_t raw = Serializer::serialize(&original);
std::cout << "Serialized: " << raw << std::endl;
// Deserialize
Data* recovered = Serializer::deserialize(raw);
std::cout << "Recovered address: " << recovered << std::endl;
// Verify pointer equality
if (recovered == &original)
std::cout << "✓ Pointer comparison: SAME ADDRESS" << std::endl;
else
std::cout << "✗ Pointer comparison: DIFFERENT ADDRESS" << std::endl;
// Verify data integrity
std::cout << "Recovered values: id=" << recovered->id
<< ", name=" << recovered->name
<< ", value=" << recovered->value << std::endl;
return 0;
}

Expected output:

Original address: 0x7fff5fbff8a0
Original values: id=42, name=test_data, value=3.14159
Serialized: 140734799801504
Recovered address: 0x7fff5fbff8a0
✓ Pointer comparison: SAME ADDRESS
Recovered values: id=42, name=test_data, value=3.14159
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);
}
main.cpp
#include "Serializer.hpp"
#include <iostream>
int main() {
Data original;
original.id = 42;
original.name = "test_data";
original.value = 3.14159;
std::cout << "Original: " << &original << std::endl;
uintptr_t raw = Serializer::serialize(&original);
std::cout << "Serialized: " << raw << std::endl;
Data* recovered = Serializer::deserialize(raw);
std::cout << "Deserialized: " << recovered << std::endl;
if (recovered == &original)
std::cout << "SUCCESS: Pointers match!" << std::endl;
else
std::cout << "FAILURE: Pointers differ!" << std::endl;
std::cout << "Data integrity: " << recovered->id << ", "
<< recovered->name << ", " << recovered->value << std::endl;
return 0;
}

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

Think before coding:

  1. How to identify type without typeinfo?

    • dynamic_cast returns NULL on failure (pointers)
    • dynamic_cast throws std::bad_cast on failure (references)
    • Try each derived type until one succeeds
  2. Why must Base be polymorphic?

    • dynamic_cast only works on polymorphic classes
    • A class is polymorphic if it has at least one virtual function
    • Virtual destructor is the cleanest solution
  3. Pointer vs Reference behavior:

    // Pointer: returns NULL on failure
    A* a = dynamic_cast<A*>(base_ptr); // NULL if not A
    // Reference: throws on failure
    A& a = dynamic_cast<A&>(base_ref); // throws if not A

Stage 1: Base and derived classes

Base.hpp
#ifndef BASE_HPP
#define BASE_HPP
class Base {
public:
virtual ~Base(); // Makes class polymorphic
};
#endif
A.hpp
#ifndef A_HPP
#define A_HPP
#include "Base.hpp"
class A : public Base {
public:
~A();
};
#endif
B.hpp
#ifndef B_HPP
#define B_HPP
#include "Base.hpp"
class B : public Base {
public:
~B();
};
#endif
C.hpp
#ifndef C_HPP
#define C_HPP
#include "Base.hpp"
class C : public Base {
public:
~C();
};
#endif

Stage 2: Implementations

Base.cpp
#include "Base.hpp"
Base::~Base() {}
A.cpp
#include "A.hpp"
A::~A() {}

(Similar for B.cpp and C.cpp)

Stage 3: Generator function

functions.cpp - generate
#include "A.hpp"
#include "B.hpp"
#include "C.hpp"
#include <cstdlib>
#include <ctime>
Base* generate() {
// Seed random number generator (only once ideally)
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; // Should never reach
}

Stage 4: Pointer identification

functions.cpp - identify pointer
#include <iostream>
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;
}

Stage 5: Reference identification

functions.cpp - identify reference
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;
}

Pointer version logic:

CodeWhat Happens
dynamic_cast<A*>(p)Attempts to cast p to A*
Returns non-NULLp points to an A object (or derived from A)
Returns NULLp does not point to an A object
if (result)Non-NULL pointer is truthy

Reference version logic:

CodeWhat Happens
dynamic_cast<A&>(p)Attempts to cast p to A&
SuccessReturns valid reference to A
FailureThrows std::bad_cast exception
(void)Discards result, we only care about success/failure
return;Exit function after successful identification

Why (void) before dynamic_cast?

// Without (void): compiler warning about unused result
dynamic_cast<A&>(p); // Warning: expression result unused
// With (void): explicitly ignoring return value
(void)dynamic_cast<A&>(p); // OK, intention is clear

1. Using typeinfo (forbidden):

// WRONG: <typeinfo> is forbidden
#include <typeinfo>
if (typeid(*p) == typeid(A)) // NOT ALLOWED
// RIGHT: Use dynamic_cast
if (dynamic_cast<A*>(p))

2. Using pointer in reference version:

// WRONG: Subject forbids using pointer in reference version
void identify(Base& p) {
Base* ptr = &p; // NOT ALLOWED
if (dynamic_cast<A*>(ptr)) ...
}
// RIGHT: Use reference dynamic_cast with try/catch
void identify(Base& p) {
try {
(void)dynamic_cast<A&>(p);
std::cout << "A" << std::endl;
return;
} catch (...) {}
// ...
}

3. Forgetting virtual destructor:

// WRONG: Base is not polymorphic
class Base {
public:
~Base(); // Not virtual!
};
// dynamic_cast will fail at compile time
// RIGHT: Virtual destructor makes it polymorphic
class Base {
public:
virtual ~Base();
};

4. Catching wrong exception:

// WRONG: Specific exception type (requires <typeinfo>)
catch (std::bad_cast& e) ...
// RIGHT: Catch all exceptions
catch (...) {}
main.cpp
#include "Base.hpp"
#include "A.hpp"
#include "B.hpp"
#include "C.hpp"
#include <iostream>
Base* generate();
void identify(Base* p);
void identify(Base& p);
int main() {
// Test random generation multiple times
for (int i = 0; i < 5; i++) {
Base* obj = generate();
std::cout << "Test " << i + 1 << ": ";
std::cout << "Pointer: ";
identify(obj);
std::cout << " Reference: ";
identify(*obj);
delete obj;
std::cout << std::endl;
}
// Test known types
std::cout << "Known A: ";
A a;
identify(&a);
identify(a);
std::cout << "Known B: ";
B b;
identify(&b);
identify(b);
std::cout << "Known C: ";
C c;
identify(&c);
identify(c);
return 0;
}

Test script:

Terminal window
# Run multiple times to see randomness
for i in {1..5}; do
./identify
echo "---"
done
Base.hpp
#ifndef BASE_HPP
#define BASE_HPP
class Base {
public:
virtual ~Base();
};
#endif
Base.cpp
#include "Base.hpp"
Base::~Base() {}
A.hpp
#ifndef A_HPP
#define A_HPP
#include "Base.hpp"
class A : public Base {
public:
~A();
};
#endif
A.cpp
#include "A.hpp"
A::~A() {}
B.hpp
#ifndef B_HPP
#define B_HPP
#include "Base.hpp"
class B : public Base {
public:
~B();
};
#endif
B.cpp
#include "B.hpp"
B::~B() {}
C.hpp
#ifndef C_HPP
#define C_HPP
#include "Base.hpp"
class C : public Base {
public:
~C();
};
#endif
C.cpp
#include "C.hpp"
C::~C() {}
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;
}
main.cpp
#include "Base.hpp"
#include "A.hpp"
#include "B.hpp"
#include "C.hpp"
#include <iostream>
Base* generate();
void identify(Base* p);
void identify(Base& p);
int main() {
for (int i = 0; i < 5; i++) {
Base* obj = generate();
std::cout << "Pointer: "; identify(obj);
std::cout << "Reference: "; identify(*obj);
delete obj;
std::cout << std::endl;
}
return 0;
}

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)