https://github.com/tigran-sargsyan-w/cpp-module-06
This module is designed to help you understand the different types of casting in C++.
https://github.com/tigran-sargsyan-w/cpp-module-06
42 42lyon 42school cpp cpp-modules cpp98 dynamic-cast inheritance object-oriented-programming oop polymorphism reinterpret-cast rtti scalar-conversion serialization static-cast type-casting
Last synced: 3 days ago
JSON representation
This module is designed to help you understand the different types of casting in C++.
- Host: GitHub
- URL: https://github.com/tigran-sargsyan-w/cpp-module-06
- Owner: tigran-sargsyan-w
- Created: 2026-02-02T21:21:53.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-04-26T14:36:42.000Z (about 1 month ago)
- Last Synced: 2026-05-06T11:49:04.057Z (26 days ago)
- Topics: 42, 42lyon, 42school, cpp, cpp-modules, cpp98, dynamic-cast, inheritance, object-oriented-programming, oop, polymorphism, reinterpret-cast, rtti, scalar-conversion, serialization, static-cast, type-casting
- Language: C++
- Homepage:
- Size: 37.1 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# C++ Module 06 β C++ Casts ππ§
β
**Status**: Completed β all exercises
π« **School**: 42 β C++ Modules (Module 06)
π
**Score**: 90/100 (One of the reviewers did not agree with the scalar converter solutionπ€)
> *Scalar conversion, pointer serialization via `reinterpret_cast`, and runtime type identification via `dynamic_cast` (without ``).*
---
## π Table of Contents
* [Description](#-description)
* [Goals of the Module](#-goals-of-the-module)
* [Exercises Overview](#-exercises-overview)
* [ex00 β ScalarConverter](#ex00--scalarconverter)
* [ex01 β Serializer](#ex01--serializer)
* [ex02 β Identify real type](#ex02--identify-real-type)
* [Requirements](#-requirements)
* [Build & Run](#-build--run)
* [Repository Layout](#-repository-layout)
* [ex00 Notes](#ex00-notes)
* [π₯ Important Edge Case: float rounding near INT limits](#-important-edge-case-float-rounding-near-int-limits)
* [π§© Clarification: Why `char: Non displayable` for 128 and not always `impossible`](#-clarification-why-char-non-displayable-for-128-and-not-always-impossible)
* [ex01 Notes](#ex01-notes)
* [π₯ Important Concept: Pointer Size & `uintptr_t`](#-important-concept-pointer-size--uintptr_t)
* [π Why `reinterpret_cast` is required here](#-why-reinterpret_cast-is-required-here)
* [β οΈ Lifetime vs Address](#οΈ-lifetime-vs-address)
* [ex02 Notes](#ex02-notes)
* [π§ Identify Real Type β Critical Nuances](#-identify-real-type--critical-nuances)
* [π Testing Tips](#-testing-tips)
* [π§Ύ 42 Notes](#-42-notes)
---
## π Description
This repository contains my solutions to **42βs C++ Module 06 (C++98)**.
The module is a deep dive into **casting and type conversions**:
* converting strings into scalars (`ex00`)
* converting pointers into integers and back (`ex01`)
* identifying the real dynamic type behind a base pointer/reference (`ex02`)
A big part of this module is not only making it work, but also understanding **why a specific cast is the correct tool**.
---
## π― Goals of the Module
Concepts practiced:
* Correct use of C++ casts: `static_cast`, `reinterpret_cast`, `dynamic_cast`
* Handling pseudo literals (`nan`, `inf`, with and without `f`) in scalar conversions
* Edge cases: overflow, precision loss, non-displayable characters
* Why `uintptr_t` exists (portable βinteger big enough to hold an addressβ)
* RTTI-style detection with `dynamic_cast` without using ``
---
## π¦ Exercises Overview
### ex00 β ScalarConverter
**Goal:** Implement a non-instantiable class `ScalarConverter` with:
* `static void convert(std::string const& literal);`
It detects the literal type, converts it to the βrealβ type first, then prints:
* `char`
* `int`
* `float`
* `double`
Must handle pseudo literals:
* `nan`, `+inf`, `-inf`
* `nanf`, `+inff`, `-inff`
---
### ex01 β Serializer
**Goal:** Implement a non-instantiable class `Serializer` with:
* `static uintptr_t serialize(Data* ptr);`
* `static Data* deserialize(uintptr_t raw);`
Create a non-empty `Data` struct and verify that:
* serializing and deserializing gives the **same address** back
* pointer equality is preserved
---
### ex02 β Identify real type
**Goal:** Create:
* `Base` (only a public virtual destructor)
* `A`, `B`, `C` inheriting publicly from `Base`
Implement:
* `Base* generate();` (randomly returns new `A/B/C`)
* `void identify(Base* p);`
* `void identify(Base& p);` (**no pointer usage inside this one**)
Forbidden:
* `` / `typeid`
---
## π Requirements
* **Compiler**: `c++`
* **Flags**: `-Wall -Wextra -Werror -std=c++98`
* **No external libraries** (no C++11+)
* **No** `printf`, `malloc`, `free` (and friends)
---
## βΆοΈ Build & Run
```bash
git clone
cd cpp-module-06
```
### ex00
```bash
cd ex00
make
./convert 0
./convert 42.0f
./convert nan
```
### ex01
```bash
cd ex01
make
./serializer
```
### ex02
```bash
cd ex02
make
./identify
```
---
## π Repository Layout
```text
cpp-module-06/
βββ ex00/
β βββ Makefile
β βββ ScalarConverter.hpp
β βββ ScalarConverter.cpp
β βββ main.cpp
β
βββ ex01/
β βββ Makefile
β βββ Serializer.hpp
β βββ Serializer.cpp
β βββ Data.hpp
β βββ main.cpp
β
βββ ex02/
βββ Makefile
βββ Base.hpp
βββ Base.cpp
βββ A.hpp / A.cpp
βββ B.hpp / B.cpp
βββ C.hpp / C.cpp
βββ main.cpp
```
---
# ex00 Notes
## π₯ Important Edge Case: float rounding near INT limits
Sometimes youβll see:
```bash
./convert 2147483647
```
and the output includes:
```
int: 2147483647
float: 2147483648.0f
```
This is **expected**.
### Why it happens
`float` has only **24 significant bits**, so near `2^31` it cannot represent every integer.
At around `2^31`:
* `2147483648 = 2^31`
* spacing between neighboring float values becomes:
**ULP = 2^(31 β 23) = 2^8 = 256**
So floats there are effectively βgrid pointsβ spaced by **256**.
Closest representable values around `2^31`:
* `2147483392 = 2147483648 β 256`
* `2147483648`
Midpoint:
* `2147483392 + 128 = 2147483520`
So:
* `2147483392 ... 2147483519` β rounds to `2147483392`
* `2147483520 ... 2147483647` β rounds to `2147483648`
Negative values behave symmetrically.
---
## π§© Clarification: Why `char: Non displayable` for 128 and not always `impossible`
You might notice:
```
Input: 128.0
char: Non displayable
```
and wonder if it should be `impossible`.
### 1) What a `char` really is in C++
In C/C++, **`char` is just a 1-byte integer type**.
Two important properties:
1. `sizeof(char) == 1` always.
2. Whether `char` is **signed** or **unsigned** is **implementation-defined**.
* On some systems: `char` behaves like `signed char` (range typically `-128..127`)
* On others: it behaves like `unsigned char` (range typically `0..255`)
So when we ask: βcan we convert to `char`?β β the answer depends on **which rule we choose**.
### 2) The subject usually wants *ASCII-like* printing
Most 42 solutions treat the `char` output as:
* Valid range: **0..127** (ASCII)
* Printable: **32..126**
* Control / DEL: `0..31` and `127` β `Non displayable`
* Everything outside `0..127` β `impossible`
Under that rule:
* `128` would be **`impossible`**
β
This is the safest choice if your evaluator/tester expects strict ASCII.
### 3) Why some implementations print `Non displayable` for 128
If your implementation checks the range using `unsigned char` limits (`0..255`), then:
* `128` is considered βrepresentable as a byteβ β so itβs **not impossible**
* but itβs not in printable ASCII (`32..126`) β so you print **Non displayable**
That logic is consistent from a *C++ type system* point of view.
### 4) Where encodings come into play (and why itβs tricky)
The reason `128` is a headache is not C++ casting β itβs **text encoding**:
* ASCII defines only **0..127**.
* Values **128..255** are **not ASCII**.
* Historically there were many βextended ASCIIβ code pages (Windows-1252, ISO-8859-1, KOI8-R, ...).
* Modern terminals often use **UTF-8**, where a single byte `0x80` is not a valid standalone character.
So printing a raw byte with value 128 can:
* show a weird symbol
* show a replacement character
* visually break output
β
Thatβs why the most evaluator-safe choice is usually: treat `char` output as ASCII-only.
### Recommended rule for maximum safety
* if value < 0 or value > 127 β `impossible`
* else if value < 32 or value == 127 β `Non displayable`
* else β print `'c'`
---
# ex01 Notes
## π₯ Important Concept: Pointer Size & `uintptr_t`
In **ex01 (Serialization)** we convert a pointer into an integer and back:
```cpp
uintptr_t raw = Serializer::serialize(ptr);
Data* again = Serializer::deserialize(raw);
```
At first glance this looks trivial β but there is an important architectural detail behind it.
### π§ Why pointer size matters
A pointer is just an address in memory.
But the **size of that address depends on the machine architecture**.
* **32-bit systems**: pointer size is 32 bits (4 GB address space)
* **16-bit systems** (very old): pointer size is 16 bits (64 KB address space)
* **64-bit systems** (modern standard): pointer size is 64 bits
* **specialized / experimental** architectures may even use **128-bit** addressing
Hardcoding a type like `unsigned long` is not portable: on some platforms it might be too small.
### β
Why we use `uintptr_t`
`uintptr_t` (from ``, C++98 compatible) is:
> an unsigned integer type capable of storing a pointer value without loss.
This means:
* On 16-bit systems β `uintptr_t` is 16 bits
* On 32-bit systems β `uintptr_t` is 32 bits
* On 64-bit systems β `uintptr_t` is 64 bits
* On 128-bit systems β it would match 128 bits
The compiler chooses the correct size automatically for the target architecture.
So instead of guessing pointer size, we rely on a **portable, architecture-safe type**.
### β οΈ Important distinction
Using `uintptr_t`:
* β guarantees we do not lose address bits
* β makes the conversion reversible
* β keeps the code portable
But it does **not**:
* β extend object lifetime
* β make a destroyed object valid again
* β protect against dangling pointers
`uintptr_t` safely stores an address.
It does not guarantee that an object still exists at that address.
---
## π Why `reinterpret_cast` is required here
In this exercise we need to convert between:
* a **pointer type** (`Data*`)
* an **integer type** (`uintptr_t`)
This is a low-level reinterpretation of the same bit pattern.
### Why not `static_cast`?
`static_cast` is for well-defined conversions like numeric conversions and safe up/down casts in inheritance.
Itβs **not meant** for arbitrary pointer β integer conversions.
### Why `reinterpret_cast`?
`reinterpret_cast` is the cast designed for:
* treating an address value as an integer
* treating an integer as an address value
So in ex01 we do:
```cpp
return reinterpret_cast(ptr);
```
and the reverse:
```cpp
return reinterpret_cast(raw);
```
Important: this cast does not validate anything β it only reinterprets.
---
## β οΈ Lifetime vs Address
A core nuance of ex01 is understanding the difference between:
* **An address** (just a number)
* **A live object** (valid memory with a valid lifetime)
### β
The address can be stored safely
`uintptr_t` can store the pointer value without losing bits.
So the conversion is reversible:
```cpp
Data* again = Serializer::deserialize(Serializer::serialize(ptr));
```
### β But the object may no longer exist
If the original objectβs lifetime ends, the address still exists as a number,
but dereferencing it becomes **Undefined Behavior**.
Typical UB situations:
* stack object leaves scope β dangling pointer
* heap object is deleted β use-after-free
> `uintptr_t` preserves the address correctly, but it cannot preserve the object.
---
# ex02 Notes
## π§ Identify Real Type β Critical Nuances
This exercise is about **polymorphism**, **RTTI behavior**, and correct use of `dynamic_cast`.
---
### π₯ Base MUST Be Polymorphic
```cpp
class Base
{
public:
virtual ~Base();
};
```
Why this is mandatory:
* `dynamic_cast` works correctly **only with polymorphic types**
* a class becomes polymorphic if it has at least one `virtual` function
Also, deleting through base pointer must be safe:
```cpp
Base* ptr = new A();
delete ptr;
```
Without a virtual destructor:
* β only `Base` destructor runs
* β derived destructor does not run
* β UB / partial destruction
So the virtual destructor guarantees:
* β RTTI works
* β proper destruction through base pointer
---
### π Implicit Upcasting in `generate()`
```cpp
return new A();
```
The function returns `Base*`, so this triggers an **implicit upcast**:
* `A* β Base*`
Equivalent to:
```cpp
A* ptrA = new A();
Base* ptrBase = ptrA;
return ptrBase;
```
This is safe because inheritance is `public`.
β
Not slicing: there is no slicing because we use pointers.
---
### β οΈ Pointer Adjustment (Subtle but Important)
In simple single inheritance, `A*` and `Base*` often have the same address.
But with multiple inheritance:
* `Base*` may require an internal offset
* the compiler automatically adjusts the pointer
Upcasting may internally modify the address to point to the correct base subobject.
---
### π `dynamic_cast` behavior
**Pointer version**:
```cpp
dynamic_cast(p);
```
* β on failure: returns `NULL`
* β on success: returns valid pointer
**Reference version**:
```cpp
dynamic_cast(p);
```
* β on failure: throws `std::bad_cast`
So the reference-based identify must use `try/catch`.
**Critical difference**:
| Cast form | On failure |
| ------------------ | ---------------- |
| `dynamic_cast` | returns `NULL` |
| `dynamic_cast` | throws exception |
Reason:
* a pointer may legally be `NULL`
* a reference must always refer to a valid object
---
### π« Why `static_cast` is wrong here (downcasting)
```cpp
A* a = static_cast(basePtr);
```
If `basePtr` actually points to `B`:
* β Undefined Behavior
* β no runtime check is performed
Only `dynamic_cast` verifies the real runtime type.
---
### π¬ What Happens "Under the Hood" (Upcast Internals)
When we write:
```cpp
return new A();
```
but the function returns `Base*`, an **implicit upcast** happens:
```
A* β Base*
```
This is NOT packaging or wrapping.
It is a compile-time conversion allowed because of `public` inheritance.
Conceptually equivalent to:
```cpp
A* ptrA = new A();
Base* ptrBase = ptrA;
return ptrBase;
```
### β οΈ Important detail
In simple single inheritance the addresses are usually identical.
With multiple inheritance, the compiler may apply an **internal pointer offset adjustment** so that the `Base*` points to the correct base subobject.
No object slicing occurs because we are using pointers.
---
## π« Why Not `static_cast`?
```cpp
A* a = static_cast(basePtr);
```
If `basePtr` actually points to `B`, this results in:
β Undefined Behavior
β No runtime type verification
`static_cast` performs no dynamic check.
Only `dynamic_cast` verifies the real runtime type using RTTI.
---
## β οΈ Where Undefined Behavior Appears
UB can happen if:
β’ You downcast using `static_cast` incorrectly
β’ Base is not polymorphic (no virtual function)
β’ You delete through a base pointer without virtual destructor
This exercise forces correct design to avoid UB.
### π Viewing what the compiler generates (optional curiosity)
To see what happens under the hood, compile without optimizations:
```bash
c++ -std=c++98 -O0 -g3 *.cpp -o identify
```
Then inspect:
```bash
objdump -d -C identify | less
```
* `-C` demangles C++ names
* `-O0` prevents optimizations from hiding steps
You can also generate assembly directly:
```bash
c++ -std=c++98 -O0 -S main.cpp -o main.s
```
---
## π Testing Tips
### ex00
```bash
./convert a
./convert 'a'
./convert 0
./convert 127
./convert 128
./convert -1
./convert 2147483647
./convert -2147483649
./convert nan
./convert +inf
./convert -inff
```
Things to verify:
* pseudo-literals output
* overflow handling (`impossible`)
* float rounding near `INT_MAX`
* consistent `char` rules (ASCII safety)
### ex01
* check pointer equality after round-trip
* print the `uintptr_t` value and compare with the original address formatting
* ensure `Data` is non-empty
### ex02
* call `generate()` multiple times
* test both `identify(ptr)` and `identify(ref)`
* verify no `` usage
---
## π§Ύ 42 Notes
* C++ modules do not use Norminette, but clean and readable code still matters.
* During evaluation youβll likely be asked to justify why you used a specific cast.
* The βreal learningβ here is understanding precision, undefined behavior, and runtime type checks β not just passing the tests.
---
If youβre a 42 student working on the same module: feel free to explore the code, get inspired, but **write your own implementation** β thatβs where the learning happens. π