C++ Module 8 – Inheritance and polymorphism
C++ casts and run-time type information (RTTI)
Choose the right C++ named cast, check downcasts with dynamic_cast and typeid, see what -fno-rtti removes, and catch bad casts with sanitizers.
What you will learn
- Choose the named cast that matches each kind of conversion
- Check a downcast with dynamic_cast and handle both kinds of failure
- Explain what -fno-rtti removes and what still works without it
- Find unsafe casts with -Wold-style-cast and UndefinedBehaviorSanitizer
Before you start
What you’ll learn · Before you start
What you will learn
- Choose the named cast that matches each kind of conversion
- Check a downcast with dynamic_cast and handle both kinds of failure
- Explain what -fno-rtti removes and what still works without it
- Find unsafe casts with -Wold-style-cast and UndefinedBehaviorSanitizer
Before you start
On this page
static_cast<Circle&>(shape).radius printed radius 2 for a square whose side is 2.0: the cast compiled on both
compilers, ran without a word and read the wrong object. A cast converts a value or a pointer to another type on
purpose, and each of the four named cast: One of the four cast operators static_cast, dynamic_cast, const_cast and reinterpret_cast. Each may perform only its own kind of conversion, so the compiler rejects one that would do something else, which a C-style cast would allow. of C++ is allowed one kind of conversion, so
that the compiler can refuse the rest; the C-style cast (T)x tries several of them in turn, which hides mistakes.
You will choose the cast that says what a conversion does, check a downcast: A conversion of a pointer or a reference to a base class into one to a derived class, such as a Shape* to a Circle*. dynamic_cast checks it while the program runs; static_cast trusts it, and a wrong one is undefined behaviour. with
dynamic_cast and the run-time type information (RTTI): The description of its own type that the compiler keeps for each class with virtual functions, which dynamic_cast's checks and typeid read while the program runs. With -fno-rtti GCC and Clang leave it out and reject those uses. (RTTI) it reads, see what -fno-rtti
removes, and find the casts that compile but break the rules, with warnings and UndefinedBehaviorSanitizer. Every
example runs on Apple clang 21 and GCC 16.
Each named cast is allowed one kind of conversion
This program makes correct use of static_cast, const_cast and reinterpret_cast, and of C++20’s
std::bit_cast; the fourth named cast, dynamic_cast, which checks a conversion inside a class hierarchy while the
program runs, has the next section to itself. The program is built with -Wold-style-cast, so a C-style cast anywhere
in it would be reported:
// One correct use of each named cast, and of std::bit_cast.
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <print>
#include <string>
enum class Grade { pass = 1, merit = 2, distinction = 3 };
// A C function from an old library: it only reads the text, but its parameter is not const.
std::size_t legacy_count_vowels(char* text) {
std::size_t n = 0;
for (; *text != '\0'; ++text) n += std::strchr("aeiouAEIOU", *text) != nullptr;
return n;
}
int main() {
// static_cast: an ordinary conversion, written down so that a reader sees it.
double average = static_cast<double>(7) / 2; // 3.5, not the 3 of integer division
int whole = static_cast<int>(3.99); // 3: the fraction is cut off
int code = static_cast<int>(Grade::merit); // a scoped enum does not convert on its own
std::println("static_cast: {} {} {}", average, whole, code);
// const_cast: remove const to call an API that will not write through the pointer.
const std::string word = "education";
std::println("const_cast: {} vowels", legacy_count_vowels(const_cast<char*>(word.c_str())));
// reinterpret_cast: look at the bytes of an object. Reading them as unsigned char is allowed.
const std::uint32_t value = 0x0A0B0C0D;
const auto* bytes = reinterpret_cast<const unsigned char*>(&value);
std::println("reinterpret_cast: bytes {:02x} {:02x} {:02x} {:02x} in memory", bytes[0], bytes[1], bytes[2], bytes[3]);
// std::bit_cast (C++20): the same bits as another type, with no undefined behaviour.
std::println("std::bit_cast: 1.0f is {:#010x}", std::bit_cast<std::uint32_t>(1.0f));
} from session import run
# named_casts.cpp built with -Wold-style-cast as well, and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra -Wold-style-cast named_casts.cpp -o named_casts && ./named_casts
g++-16 -std=c++23 -Wall -Wextra -Wold-style-cast named_casts.cpp -o named_casts-gcc && ./named_casts-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra -Wold-style-cast named_casts.cpp -o named_casts && ./named_casts static_cast: 3.5 3 2 const_cast: 5 vowels reinterpret_cast: bytes 0d 0c 0b 0a in memory std::bit_cast: 1.0f is 0x3f800000 $ g++-16 -std=c++23 -Wall -Wextra -Wold-style-cast named_casts.cpp -o named_casts-gcc && ./named_casts-gcc static_cast: 3.5 3 2 const_cast: 5 vowels reinterpret_cast: bytes 0d 0c 0b 0a in memory std::bit_cast: 1.0f is 0x3f800000
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 named_casts_session.py
Each line is a cast you will meet in real code. static_cast performs ordinary conversions:
static_cast<double>(7) / 2 makes the division happen in floating point, and static_cast<int>(Grade::merit) gives a
scoped enum’s number, 2. const_cast removes const: it lets a const std::string reach a C function whose
parameter lacks const, which is safe only because that function never writes. reinterpret_cast treats a pointer as
another pointer type or as a number: here as const unsigned char*, to show the bytes of a number in memory, lowest
first on this byte order: The order in which the bytes of an integer are stored in memory: little-endian puts the least significant byte first, big-endian the most significant byte first. std::byteswap reverses the order, turning one into the other. More in “C++ bitwise operators and the <bit> header” computer, which the standard allows for any object. And when
you want the bits of one type as another type, std::bit_cast copies them without breaking any rule: 1.0f is
0x3f800000. The four named casts, compared in pairs:
| Criterion | static_cast | dynamic_cast |
|---|---|---|
| Converts | Numbers, enums, void* back to T*, base to derived | Pointers and references in a class hierarchy |
| Checked | When compiling | While the program runs |
| Use it for | Making a conversion visible | A downcast that may fail |
| Breaks when | A downcast meets another class | RTTI is turned off |
| Criterion | const_cast | reinterpret_cast |
|---|---|---|
| Converts | Only const and volatile | Pointers to other pointer types or integers |
| Checked | When compiling | Only that both are pointers or integers |
| Use it for | Calling an old API that will not write | Looking at raw bytes |
| Breaks when | Something writes to a const object | The pointer is used as the wrong type |
When a conversion needs a cast, ask five questions in order, and stop at the first yes:
Choosing the cast that says what a conversion does
Text description of the diagram
The diagram is a column of five questions, asked from top to bottom. Each box names the cast to write when the answer is yes; an arrow labelled "no" leads to the next question.
- Do you need a value as another type, such as an int as a double? Yes: static_cast.
- Do you need to turn a pointer or reference to a base class into one to a derived class? Yes: dynamic_cast, which checks the object's real type; static_cast only when the type is certain.
- Do you need to remove const to call code that will not write through the pointer? Yes: const_cast.
- Do you need the same bits seen as another type, such as the bits of a float as an integer? Yes: std::bit_cast.
- Do you need the raw bytes of an object, or an address as a number? Yes: reinterpret_cast.
- If every answer is no, the last arrow leads to "Rethink the design. A C-style cast, (T)x, is never the answer."
Checkpoint
Check: What each named cast may do
A checked downcast asks the object’s real type
A downcast turns a pointer or reference to a base class into one to a derived class. dynamic_cast checks it while
the program runs, against the object’s dynamic type: The class of the object an expression refers to while the program runs: a Shape* that points to a Circle reaches an object of the dynamic type Circle. Calls to virtual functions are chosen from it. More in “Virtual functions, override and virtual destructors”, so it works only for classes with
at least one virtual function:
// Checked downcasts: dynamic_cast to a pointer gives nullptr when the object is of another class, and dynamic_cast
// to a reference throws std::bad_cast. typeid names the dynamic type.
#include <memory>
#include <print>
#include <typeinfo>
#include <vector>
struct Shape {
virtual ~Shape() = default;
};
struct Circle : Shape {
double radius = 0.5;
};
struct Square : Shape {
double side = 2.0;
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Square>());
for (const auto& owner : shapes) {
Shape& shape = *owner;
if (auto* circle = dynamic_cast<Circle*>(&shape)) std::println("a circle, radius {}", circle->radius);
else std::println("not a circle: typeid says {}", typeid(shape).name());
}
try {
Circle& circle = dynamic_cast<Circle&>(*shapes[1]);
std::println("radius {}", circle.radius);
} catch (const std::bad_cast& e) {
std::println("dynamic_cast to Circle& threw std::bad_cast: {}", e.what());
}
const Shape& first = *shapes[0];
std::println("first is exactly a Circle: {}", typeid(first) == typeid(Circle));
} from session import run
# downcast.cpp built and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra downcast.cpp -o downcast && ./downcast
g++-16 -std=c++23 -Wall -Wextra downcast.cpp -o downcast-gcc && ./downcast-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra downcast.cpp -o downcast && ./downcast a circle, radius 0.5 not a circle: typeid says 6Square dynamic_cast to Circle& threw std::bad_cast: std::bad_cast first is exactly a Circle: true $ g++-16 -std=c++23 -Wall -Wextra downcast.cpp -o downcast-gcc && ./downcast-gcc a circle, radius 0.5 not a circle: typeid says 6Square dynamic_cast to Circle& threw std::bad_cast: std::bad_cast first is exactly a Circle: true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 downcast_session.py
Step through the loop and the cast to a reference, and watch where each answer comes from:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Is it a Circle?. The loop casts its first Shape& with dynamic_cast<Circle*>. The cast does not trust the static type: it looks at the object itself.
- The vptr leads to the answer. With GCC and Clang the object's vptr leads to its class's virtual table, and the table holds a pointer to the type information of that class: Circle.
- A Circle*. The object is a Circle, so the cast returns a pointer to it, and the program prints a circle, radius 0.5.
- The second shape. The same cast on shapes[1] finds the type information of Square. A Square is not a Circle, so the cast to a pointer gives nullptr, and the if goes to its else.
- typeid names the type. The else asks typeid(shape).name(). It reads the same type information and prints 6Square, the class name with its length in front, as both compilers write names.
- A reference cannot be null. dynamic_cast<Circle&>(*shapes[1]) fails the same check, and a reference has no null value to give, so the cast throws std::bad_cast, which the program catches.
- Exactly a Circle?. typeid(first) == typeid(Circle) compares the dynamic type of the first shape with Circle. They are the same class, so the program prints true.
A failed dynamic_cast to a pointer gives a null pointer, so the first loop can ask “is this a circle?” and try
something else. A reference cannot be null, so a failed cast to a reference throws std::bad_cast. Rules C.147 and
C.148 of the C++ Core Guidelines turn that into a choice: cast to a reference when failure means a bug, to a pointer
when it is a valid answer.
typeid(shape) returns a std::type_info for the dynamic type, and comparing two of them tells whether an object is
exactly a given class. Its name() is not portable: both compilers print 6Square, the class name with its length in
front, because both encode names as the Itanium C++ ABI describes.
Testing for a family of classes with typeid
typeid(shape) == typeid(Circle) is true only for an object whose class is exactly Circle. For a class derived
from Circle it is false, while dynamic_cast<Circle*> still succeeds: use the cast when any kind of circle will
do.
Every dynamic_cast and typeid asks a question that a virtual function could often answer without one. Rule C.153
of the Guidelines says to prefer a virtual function to casting, and rule C.146 keeps dynamic_cast for the cases where
navigating the hierarchy is unavoidable, such as an object handed back by a plugin or a library.
Checkpoint
Check: Checked downcasts
Without RTTI, checked downcasts and typeid stop compiling
Some projects turn RTTI off to save the space the type information takes. The LLVM compiler is one: its coding
standards rule out RTTI and exceptions to keep its code and its executables smaller. Built with -fno-rtti, the same
downcast.cpp stops compiling, and this second program shows what still works:
// What still works when a program is built with -fno-rtti: upcasts, casts to void* and exceptions. Built with
// -DTYPEID_INT, it also asks for the type_info of int.
#include <print>
#include <stdexcept>
#include <typeinfo>
struct Shape {
virtual ~Shape() = default;
};
struct Circle : Shape {
double radius = 0.5;
};
int main() {
Circle c;
Shape* up = dynamic_cast<Shape*>(&c); // to a base class: no run-time check is needed
void* whole = dynamic_cast<void*>(up); // to void*: the address of the complete object
std::println("upcast works: {}, cast to void* works: {}", up == &c, whole == &c);
try {
throw std::runtime_error("thrown and caught");
} catch (const std::exception& e) {
std::println("exceptions work: {}", e.what());
}
#ifdef TYPEID_INT
std::println("{}", typeid(int).name());
#endif
} from session import run
# downcast.cpp again, built with -fno-rtti; then what does work without RTTI, and typeid of int.
run("""
clang++ -std=c++23 -fno-rtti -c downcast.cpp
g++-16 -std=c++23 -fno-rtti -c downcast.cpp
clang++ -std=c++23 -fno-rtti without_rtti.cpp -o without_rtti && ./without_rtti
g++-16 -std=c++23 -fno-rtti without_rtti.cpp -o without_rtti-gcc && ./without_rtti-gcc
clang++ -std=c++23 -fno-rtti -DTYPEID_INT -c without_rtti.cpp
g++-16 -std=c++23 -fno-rtti -DTYPEID_INT -c without_rtti.cpp
""", max_lines=14) Output
$ clang++ -std=c++23 -fno-rtti -c downcast.cpp
downcast.cpp:25:28: error: use of dynamic_cast requires -frtti
25 | if (auto* circle = dynamic_cast<Circle*>(&shape)) std::println("a circle, radius {}", circle->radius);
| ^
downcast.cpp:26:59: error: use of typeid requires -frtti
26 | else std::println("not a circle: typeid says {}", typeid(shape).name());
| ^
downcast.cpp:30:26: error: use of dynamic_cast requires -frtti
30 | Circle& circle = dynamic_cast<Circle&>(*shapes[1]);
| ^
downcast.cpp:36:51: error: use of typeid requires -frtti
36 | std::println("first is exactly a Circle: {}", typeid(first) == typeid(Circle));
| ^
downcast.cpp:36:68: error: use of typeid requires -frtti
36 | std::println("first is exactly a Circle: {}", typeid(first) == typeid(Circle));
[2 more lines]
[exit status 1]
$ g++-16 -std=c++23 -fno-rtti -c downcast.cpp
downcast.cpp: In function 'int main()':
downcast.cpp:25:28: error: 'dynamic_cast' not permitted with '-fno-rtti'
25 | if (auto* circle = dynamic_cast<Circle*>(&shape)) std::println("a circle, radius {}", circle->radius);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
downcast.cpp:26:66: error: cannot use 'typeid' with '-fno-rtti'
26 | else std::println("not a circle: typeid says {}", typeid(shape).name());
| ^~~~~
downcast.cpp:30:26: error: 'dynamic_cast' not permitted with '-fno-rtti'
30 | Circle& circle = dynamic_cast<Circle&>(*shapes[1]);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
downcast.cpp:36:58: error: cannot use 'typeid' with '-fno-rtti'
36 | std::println("first is exactly a Circle: {}", typeid(first) == typeid(Circle));
| ^~~~~
downcast.cpp:36:81: error: cannot use 'typeid' with '-fno-rtti'
[2 more lines]
[exit status 1]
$ clang++ -std=c++23 -fno-rtti without_rtti.cpp -o without_rtti && ./without_rtti
upcast works: true, cast to void* works: true
exceptions work: thrown and caught
$ g++-16 -std=c++23 -fno-rtti without_rtti.cpp -o without_rtti-gcc && ./without_rtti-gcc
upcast works: true, cast to void* works: true
exceptions work: thrown and caught
$ clang++ -std=c++23 -fno-rtti -DTYPEID_INT -c without_rtti.cpp
without_rtti.cpp:25:24: error: use of typeid requires -frtti
25 | std::println("{}", typeid(int).name());
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -fno-rtti -DTYPEID_INT -c without_rtti.cpp
without_rtti.cpp: In function 'int main()':
without_rtti.cpp:25:34: error: cannot use 'typeid' with '-fno-rtti'
25 | std::println("{}", typeid(int).name());
| ^
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 no_rtti_session.py
Each compiler rejects every dynamic_cast that needs a run-time check and every typeid. GCC’s manual explains that a
dynamic_cast that needs no type information still works, and the session shows it on both compilers: the
upcast: A conversion of a pointer or a reference to a derived class into one to a base class, such as a Circle* to a Shape*. For a public base it is implicit and always safe, and it needs no check while the program runs. to Shape*, and the cast to void*, which returns the address of the complete
object. Exceptions work too; GCC’s manual says it generates the type information that exception handling needs, and
the Clang build behaved the same. Both compilers reject even typeid(int), whose answer is known when compiling. The
session, line by line:
| Criterion | With RTTI | With -fno-rtti |
|---|---|---|
| dynamic_cast<Circle*>(&shape) | Checks the object while running | Error: it needs the check |
| typeid(shape), typeid(int) | The type information | Error, even for int |
| dynamic_cast<Shape*>(&c) | An upcast: works | Works: no check needed |
| dynamic_cast<void*>(up) | The complete object's address | Works |
| throw and catch | Works | Works |
Code meant for builds without RTTI therefore uses virtual functions, or a kind() function of its own, instead of
dynamic_cast.
Checkpoint
Check: Builds without RTTI
An unchecked downcast reads the wrong object
static_cast can also downcast, faster than dynamic_cast, because it checks nothing while the program runs:
// static_cast downcasts without checking: radius_of() trusts that every Shape it gets is a Circle.
#include <print>
struct Shape {
virtual ~Shape() = default;
};
struct Circle : Shape {
double radius = 0.5;
};
struct Square : Shape {
double side = 2.0;
};
double radius_of(Shape& shape) { return static_cast<Circle&>(shape).radius; }
int main() {
Square tile;
std::println("radius {}", radius_of(tile)); // tile is not a Circle: undefined behaviour
} from session import run
# unchecked_downcast.cpp without sanitizers, with -fsanitize=undefined, and with -fsanitize=vptr. Addresses change on
# every run, so they are shown as 0x…, and the memory dump under each report is left out.
run("""
clang++ -std=c++23 -Wall -Wextra unchecked_downcast.cpp -o plain && ./plain
clang++ -std=c++23 -fsanitize=undefined unchecked_downcast.cpp -o ub && ./ub
clang++ -std=c++23 -fsanitize=undefined,vptr unchecked_downcast.cpp -o vptr && ./vptr
g++-16 -std=c++23 -fsanitize=undefined unchecked_downcast.cpp -o ub-gcc && ./ub-gcc
""", mask_addresses=True) Output
$ clang++ -std=c++23 -Wall -Wextra unchecked_downcast.cpp -o plain && ./plain radius 2 $ clang++ -std=c++23 -fsanitize=undefined unchecked_downcast.cpp -o ub && ./ub radius 2 $ clang++ -std=c++23 -fsanitize=undefined,vptr unchecked_downcast.cpp -o vptr && ./vptr unchecked_downcast.cpp:14:41: runtime error: downcast of address 0x… which does not point to an object of type 'Circle' 0x…: note: object is of type 'Square' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior unchecked_downcast.cpp:14:41 unchecked_downcast.cpp:14:41: runtime error: member access within address 0x… which does not point to an object of type 'Circle' 0x…: note: object is of type 'Square' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior unchecked_downcast.cpp:14:41 radius 2 $ g++-16 -std=c++23 -fsanitize=undefined unchecked_downcast.cpp -o ub-gcc && ./ub-gcc unchecked_downcast.cpp:14:69: runtime error: member access within address 0x… which does not point to an object of type 'Circle' 0x…: note: object is of type 'Square' radius 2
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 unchecked_downcast_session.py
The program “works”: it prints radius 2, which is the square’s side read as if it were a circle’s radius, because
both members happen to sit at the same offset. Step through what the cast makes the program read:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A Shape& to a Square. main passes tile, a Square, to radius_of(), which takes a Shape&. Inside the function nothing says which class the object has.
- Checked when compiling only. static_cast<Circle&>(shape) compiles because Circle derives from Shape. That is all it checks: it never looks at the object while the program runs.
- Where a Circle keeps its radius. .radius is read where a Circle keeps it, 8 bytes in, after the vptr. In tile those 8 bytes hold side, 2.0.
- A plausible wrong number. Every build prints radius 2. Reading tile as a Circle is undefined behaviour, and a quiet wrong value is a likely symptom.
- Only the vptr check sees it. UndefinedBehaviorSanitizer's vptr check compares the object's vptr with Circle and reports the access. GCC's -fsanitize=undefined includes it; Clang needs -fsanitize=undefined,vptr.
That is undefined behaviour: What a program does when it breaks a rule for which the C++ standard sets no requirements, such as reading past the end of an array. The compiler may assume it never happens, so the result can change with the compiler and the optimisation level. More in “Object lifetime and undefined behaviour in C++”, and a quiet wrong number is a likely symptom. The
session replaces the addresses in the reports with 0x…, because they change on every run, and leaves out the memory
dump that follows each report. The four builds show a difference between the compilers that is easy to miss:
| Criterion | Reported | Printed |
|---|---|---|
| clang++, no sanitizer | Nothing | radius 2 |
| clang++ -fsanitize=undefined | Nothing | radius 2 |
| clang++ -fsanitize=undefined,vptr | The downcast and the member access | radius 2 |
| g++-16 -fsanitize=undefined | The member access | radius 2 |
GCC includes the vptr check of its sanitizer: A compiler option, such as -fsanitize=address or -fsanitize=undefined, that builds extra checks into a program, so that memory errors and undefined behaviour are reported, with their source line, while it runs. More in “Finding memory bugs with AddressSanitizer and UBSan”, which compares the object’s
vptr: The hidden pointer that GCC and Clang store in each object of a class with virtual functions. It points to the virtual table of the object's class and takes 8 bytes on a 64-bit computer, however many virtual functions the class has. More in “Virtual functions, override and virtual destructors” with the type of the cast, in -fsanitize=undefined, and reports the bad access. Clang’s
documentation says that vptr is not part of its undefined group, and indeed -fsanitize=undefined alone printed
nothing; with -fsanitize=undefined,vptr Clang reports both the downcast and the member access. Use static_cast for
a downcast only when the type is certain, and add vptr to Clang’s sanitizer builds.
Checkpoint
Check: Unchecked downcasts
Casting away const cannot make an object writable
const_cast changes the type of a pointer or reference, not the object it refers to:
// const_cast removes const from a pointer, but it cannot make a const object writable.
#include <print>
int main() {
const int limit = 10;
int* p = const_cast<int*>(&limit); // compiles
*p = 20; // undefined behaviour: limit was defined const
int copy = limit;
std::println("*p is {}, a copy of limit is {}", *p, copy);
} from session import run
# const_violation.cpp without optimisation and with -O2, on both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra const_violation.cpp -o cv && ./cv
clang++ -std=c++23 -Wall -Wextra -O2 const_violation.cpp -o cv-o2 && ./cv-o2
g++-16 -std=c++23 -Wall -Wextra const_violation.cpp -o cv-gcc && ./cv-gcc
g++-16 -std=c++23 -Wall -Wextra -O2 const_violation.cpp -o cv-gcc-o2 && ./cv-gcc-o2
""") Output
$ clang++ -std=c++23 -Wall -Wextra const_violation.cpp -o cv && ./cv *p is 20, a copy of limit is 10 $ clang++ -std=c++23 -Wall -Wextra -O2 const_violation.cpp -o cv-o2 && ./cv-o2 *p is 20, a copy of limit is 10 $ g++-16 -std=c++23 -Wall -Wextra const_violation.cpp -o cv-gcc && ./cv-gcc *p is 20, a copy of limit is 10 $ g++-16 -std=c++23 -Wall -Wextra -O2 const_violation.cpp -o cv-gcc-o2 && ./cv-gcc-o2 *p is 20, a copy of limit is 10
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 const_violation_session.py
Writing through p to limit, an object defined const, is undefined behaviour. All four builds show why: *p reads
20, yet a copy of limit is 10. Step through the lines of main:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A constant. const int limit = 10; defines an object that can never change, so the compilers may use 10 wherever the program reads limit.
- A pointer without const. const_cast<int*>(&limit) compiles: it changes the type of the pointer, not the object. p is an int* that points to a const object.
- The write. *p = 20; writes to an object defined const, which is undefined behaviour. In these builds the store happened: *p reads 20 afterwards.
- The copy. int copy = limit; gets 10: the compilers used the value a const int initialised with 10 must hold, not what the memory holds now.
- Two values for one object. All four builds, with and without -O2, print *p is 20, a copy of limit is 10. Undefined behaviour gave one object two values.
The compilers know that a const int initialised with 10 can never change, so they use 10 wherever limit appears.
Rule ES.50 of the C++ Core Guidelines says not to cast away const. The legitimate use is the one in
named_casts.cpp: an old API whose parameter lacks const although it never writes. What counts is how the object
itself was defined: a std::string that is not const may be changed through a const_cast of a const reference
to it, because only the reference was const.
Checkpoint
Check: Casting away const
Reinterpreting a pointer never moves it to a base
The previous lesson showed that the second base of a class sits at an offset inside the object. static_cast knows
that offset; reinterpret_cast does not:
// static_cast to a second base class moves the pointer to that base's part; reinterpret_cast keeps the address and
// only pretends. Nothing is called through the wrong pointer: that would be undefined behaviour.
#include <cstddef>
#include <print>
#include <string>
struct Speaker {
virtual ~Speaker() = default;
virtual void play(const std::string& sound) { std::println("plays {}", sound); }
};
struct Microphone {
virtual ~Microphone() = default;
virtual std::string record() { return "a voice note"; }
};
struct Headset : Speaker, Microphone {};
// How far b is from a, in bytes.
std::ptrdiff_t distance(const void* a, const void* b) {
return static_cast<const std::byte*>(b) - static_cast<const std::byte*>(a);
}
int main() {
Headset h;
auto* by_static = static_cast<Microphone*>(&h);
auto* by_reinterpret = reinterpret_cast<Microphone*>(&h);
std::println("static_cast: the Microphone part is {} bytes into the Headset", distance(&h, by_static));
std::println("reinterpret_cast: it points {} bytes in, at the Speaker part", distance(&h, by_reinterpret));
std::println("static_cast result works: {}", by_static->record());
} from session import run
# adjust.cpp built and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra adjust.cpp -o adjust && ./adjust
g++-16 -std=c++23 -Wall -Wextra adjust.cpp -o adjust-gcc && ./adjust-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra adjust.cpp -o adjust && ./adjust
adjust.cpp:25:28: warning: 'reinterpret_cast' from class 'Headset *' to its base at non-zero offset 'Microphone *' behaves differently from 'static_cast' [-Wreinterpret-base-class]
25 | auto* by_reinterpret = reinterpret_cast<Microphone*>(&h);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
adjust.cpp:25:28: note: use 'static_cast' to adjust the pointer correctly while upcasting
25 | auto* by_reinterpret = reinterpret_cast<Microphone*>(&h);
| ^~~~~~~~~~~~~~~~
| static_cast
1 warning generated.
static_cast: the Microphone part is 8 bytes into the Headset
reinterpret_cast: it points 0 bytes in, at the Speaker part
static_cast result works: a voice note
$ g++-16 -std=c++23 -Wall -Wextra adjust.cpp -o adjust-gcc && ./adjust-gcc
static_cast: the Microphone part is 8 bytes into the Headset
reinterpret_cast: it points 0 bytes in, at the Speaker part
static_cast result works: a voice note
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 adjust_session.py
Step through the two casts and the pointer each one gives:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- One Headset. Headset h; holds a Speaker part at offset 0 and a Microphone part at offset 8, as in the previous lesson.
- static_cast moves the pointer. static_cast<Microphone*>(&h) knows the layout of Headset, so it adds 8: by_static points at the Microphone part, and calling record() through it works.
- reinterpret_cast does not. reinterpret_cast<Microphone*>(&h) keeps the address as it is, 0 bytes in. by_reinterpret claims to point at a Microphone but points at the Speaker part.
- Only Clang warns. Clang 21 warns by default, with -Wreinterpret-base-class, and proposes static_cast; GCC 16 says nothing. A call through by_reinterpret would be undefined behaviour, so the program never makes one.
static_cast<Microphone*> moves the pointer 8 bytes to the Microphone part. reinterpret_cast<Microphone*> keeps
the address of the Speaker part, so a call through it would run on the wrong object. Clang 21 warns about this cast
by default, with -Wreinterpret-base-class; GCC 16 says nothing. Keep reinterpret_cast for raw bytes and addresses,
and use std::bit_cast when you want the value of one type as another: reading a float through a std::uint32_t*,
the old way to get its bits, breaks the aliasing rule: The rule that an object may be read only through its own type, its signed or unsigned twin, or a byte type such as unsigned char or std::byte. Optimisers rely on it, so reading a float through a std::uint32_t* is undefined behaviour. More in “Memory layout: sizeof, alignment and padding”.
Checkpoint
Check: What reinterpret_cast does not do
A C-style cast picks whichever cast compiles
The standard lists what a C-style cast: The cast notation (T)x that C++ took over from C. It performs the first of const_cast, static_cast, static_cast then const_cast, reinterpret_cast and reinterpret_cast then const_cast that can express the conversion, even to a private base. (T)x may be: a const_cast, a static_cast,
a static_cast followed by a const_cast, a reinterpret_cast, or a reinterpret_cast followed by a const_cast.
The first of them that can express the conversion is the one you get, and its static_cast may even convert to a base
class that is private.
// Two C-style casts that compile without a word. Built with -DNAMED, the same conversions are written as
// static_cast, and the compiler checks them.
struct Account {
long balance = 50'000;
};
class FrozenAccount : private Account {}; // private: nobody outside may treat it as an Account
struct Shape {
virtual ~Shape() = default;
};
struct Circle : Shape {};
struct Square : Shape {};
#ifdef NAMED
Account* unfreeze(FrozenAccount* f) { return static_cast<Account*>(f); }
Square* as_square(Circle* c) { return static_cast<Square*>(c); }
#else
Account* unfreeze(FrozenAccount* f) { return (Account*)f; } // ignores the private base
Square* as_square(Circle* c) { return (Square*)c; } // a Circle is never a Square
#endif from session import run
# c_style.cpp compiled as written, then with -Wold-style-cast, then with the casts written as static_cast.
run("""
clang++ -std=c++23 -Wall -Wextra -c c_style.cpp
g++-16 -std=c++23 -Wall -Wextra -c c_style.cpp
clang++ -std=c++23 -Wall -Wextra -Wold-style-cast -c c_style.cpp
g++-16 -std=c++23 -Wall -Wextra -Wold-style-cast -c c_style.cpp
clang++ -std=c++23 -Wall -Wextra -DNAMED -c c_style.cpp
g++-16 -std=c++23 -Wall -Wextra -DNAMED -c c_style.cpp
""", max_lines=12) Output
$ clang++ -std=c++23 -Wall -Wextra -c c_style.cpp
$ g++-16 -std=c++23 -Wall -Wextra -c c_style.cpp
$ clang++ -std=c++23 -Wall -Wextra -Wold-style-cast -c c_style.cpp
c_style.cpp:19:46: warning: use of old-style cast [-Wold-style-cast]
19 | Account* unfreeze(FrozenAccount* f) { return (Account*)f; } // ignores the private base
| ^ ~
c_style.cpp:20:39: warning: use of old-style cast [-Wold-style-cast]
20 | Square* as_square(Circle* c) { return (Square*)c; } // a Circle is never a Square
| ^ ~
2 warnings generated.
$ g++-16 -std=c++23 -Wall -Wextra -Wold-style-cast -c c_style.cpp
c_style.cpp: In function 'Account* unfreeze(FrozenAccount*)':
c_style.cpp:19:56: warning: use of old-style cast to 'struct Account*' [-Wold-style-cast]
19 | Account* unfreeze(FrozenAccount* f) { return (Account*)f; } // ignores the private base
| ^
| -----------
| reinterpret_cast<Account*> (f)
c_style.cpp: In function 'Square* as_square(Circle*)':
c_style.cpp:20:48: warning: use of old-style cast to 'struct Square*' [-Wold-style-cast]
20 | Square* as_square(Circle* c) { return (Square*)c; } // a Circle is never a Square
| ^
| ----------
| reinterpret_cast<Square*> (c)
$ clang++ -std=c++23 -Wall -Wextra -DNAMED -c c_style.cpp
c_style.cpp:16:68: error: cannot cast 'FrozenAccount' to its private base class 'Account'
16 | Account* unfreeze(FrozenAccount* f) { return static_cast<Account*>(f); }
| ^
c_style.cpp:7:23: note: declared private here
7 | class FrozenAccount : private Account {}; // private: nobody outside may treat it as an Account
| ^~~~~~~~~~~~~~~
c_style.cpp:17:39: error: static_cast from 'Circle *' to 'Square *', which are not related by inheritance, is not allowed
17 | Square* as_square(Circle* c) { return static_cast<Square*>(c); }
| ^~~~~~~~~~~~~~~~~~~~~~~
2 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -DNAMED -c c_style.cpp
c_style.cpp: In function 'Account* unfreeze(FrozenAccount*)':
c_style.cpp:16:69: error: 'Account' is an inaccessible base of 'FrozenAccount'
16 | Account* unfreeze(FrozenAccount* f) { return static_cast<Account*>(f); }
| ^
c_style.cpp: In function 'Square* as_square(Circle*)':
c_style.cpp:17:39: error: invalid 'static_cast' from type 'Circle*' to type 'Square*'
17 | Square* as_square(Circle* c) { return static_cast<Square*>(c); }
| ^~~~~~~~~~~~~~~~~~~~~~~
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 c_style_session.py
Step through how the compiler reads the two casts of c_style.cpp:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- One notation, five meanings. as_square() writes (Square*)c. A C-style cast is not a cast of its own: the standard lists five ways to read it and uses the first that can express the conversion.
- Only const?. A const_cast may only add or remove const and volatile. A Circle* and a Square* differ in more than that, so the first reading fails.
- Related classes?. A static_cast between pointers to classes needs one class to derive from the other. Circle and Square only share a base, so it fails, and so does a static_cast followed by a const_cast.
- Anything goes. A reinterpret_cast may turn any object pointer into another. It is the first reading that works, so (Square*)c quietly becomes reinterpret_cast<Square*>(c).
- The private base. In unfreeze(), (Account*)f already works as the second reading: a static_cast written as a C-style cast may convert to a private base, which a real static_cast refuses.
- Make the meaning visible. With -Wold-style-cast both compilers point at the two casts, and GCC proposes reinterpret_cast for each. Written as static_cast, both are rejected, which is the point.
Both functions compile silently on both compilers. unfreeze() reaches into a base class that FrozenAccount keeps
private, and as_square() turns a Circle* into a Square*, which can only be a reinterpret_cast. With
-Wold-style-cast, both compilers point at the two casts, and GCC even prints a fix-it: A line under a diagnostic's source line that shows the text to type, such as std::cout under cout. Clang and GCC print one when they can guess the change; check that it is what you meant before you apply it. More in “Reading C++ compiler errors and fixing old code”,
reinterpret_cast for each. Written as static_cast, the same conversions are rejected, which is the point: a named
cast states what you meant, so the compiler can refuse what you did not mean. Rule ES.49 of the C++ Core Guidelines
says to use named casts, and rule ES.48 to avoid casts altogether where the design allows it.
Checkpoint
Check: C-style casts
Practise: five C-style casts to replace
Clean up sensor.h, whose five C-style casts stop the tests from building with -Wold-style-cast -Werror:
C header · sensor.h
// Exercise: the tests build with -Wold-style-cast -Werror, so every C-style cast below stops them. Replace each one
// with the cast, or the function, that says what it does, and make every check in sensor_tests.cpp pass.
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string>
struct Reading {
virtual ~Reading() = default;
double celsius = 0;
};
struct HumidReading : Reading {
int humidity = 0; // per cent
};
enum class Unit { celsius = 1, fahrenheit = 2 };
// 1. The mean of readings stored as whole tenths of a degree: 215 is 21.5 °C.
inline double mean_celsius(int total_tenths, int count) { return (double)total_tenths / count / 10; }
// 2. The code of a unit, for an old file format.
inline int unit_code(Unit unit) { return (int)unit; }
// 3. A C library function that only reads its text, but takes a char*.
inline std::size_t legacy_length(char* text) { return std::strlen(text); }
inline std::size_t label_length(const std::string& label) { return legacy_length((char*)label.c_str()); }
// 4. The bits of a float, for a checksum.
inline std::uint32_t bits_of(float value) { return *(std::uint32_t*)&value; }
// 5. The humidity of a reading, or -1 when the reading has none.
inline int humidity_of(Reading* reading) {
HumidReading* humid = (HumidReading*)reading;
return humid != nullptr ? humid->humidity : -1;
} C++ · sensor_tests.cpp
// Sample tests for sensor.h. Each check prints what it expects when it fails; the exit status is 0 only when every
// check passes.
#include <print>
#include <string>
#include "sensor.h"
namespace {
int checks = 0;
int passed = 0;
void check(bool ok, const char* what) {
++checks;
if (ok) ++passed;
else std::println("FAILED: {}", what);
}
} // namespace
int main() {
check(mean_celsius(645, 3) == 21.5, "the mean of 64.5 °C over three readings is 21.5 °C");
check(unit_code(Unit::fahrenheit) == 2, "the code of fahrenheit is 2");
check(label_length("kitchen") == 7, "the label kitchen has 7 characters");
check(bits_of(1.0f) == 0x3f80'0000, "the bits of 1.0f are 0x3f800000");
HumidReading humid;
humid.humidity = 55;
Reading plain;
check(humidity_of(&humid) == 55, "a humid reading reports its humidity");
check(humidity_of(&plain) == -1, "a plain reading has no humidity");
std::println("{} of {} checks passed", passed, checks);
return passed == checks ? 0 : 1;
} from session import run
# The sample tests built against the starter code with both compilers; -Werror turns each warning into an error.
run("""
clang++ -std=c++23 -Wall -Wextra -Wold-style-cast -Werror sensor_tests.cpp -o sensor_tests && ./sensor_tests
g++-16 -std=c++23 -Wall -Wextra -Wold-style-cast -Werror sensor_tests.cpp -o sensor_tests-gcc && ./sensor_tests-gcc
""", max_lines=40) Output
$ clang++ -std=c++23 -Wall -Wextra -Wold-style-cast -Werror sensor_tests.cpp -o sensor_tests && ./sensor_tests
In file included from sensor_tests.cpp:6:
./sensor.h:20:66: error: use of old-style cast [-Werror,-Wold-style-cast]
20 | inline double mean_celsius(int total_tenths, int count) { return (double)total_tenths / count / 10; }
| ^ ~~~~~~~~~~~~
./sensor.h:23:42: error: use of old-style cast [-Werror,-Wold-style-cast]
23 | inline int unit_code(Unit unit) { return (int)unit; }
| ^ ~~~~
./sensor.h:27:82: error: use of old-style cast [-Werror,-Wold-style-cast]
27 | inline std::size_t label_length(const std::string& label) { return legacy_length((char*)label.c_str()); }
| ^ ~~~~~~~~~~~~~
./sensor.h:30:53: error: use of old-style cast [-Werror,-Wold-style-cast]
30 | inline std::uint32_t bits_of(float value) { return *(std::uint32_t*)&value; }
| ^ ~~~~~~
./sensor.h:34:27: error: use of old-style cast [-Werror,-Wold-style-cast]
34 | HumidReading* humid = (HumidReading*)reading;
| ^ ~~~~~~~
5 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -Wold-style-cast -Werror sensor_tests.cpp -o sensor_tests-gcc && ./sensor_tests-gcc
In file included from sensor_tests.cpp:6:
sensor.h: In function 'double mean_celsius(int, int)':
sensor.h:20:74: error: use of old-style cast to 'double' [-Werror=old-style-cast]
20 | inline double mean_celsius(int total_tenths, int count) { return (double)total_tenths / count / 10; }
| ^~~~~~~~~~~~
| --------------------
| static_cast<double> (total_tenths)
sensor.h: In function 'int unit_code(Unit)':
sensor.h:23:47: error: use of old-style cast to 'int' [-Werror=old-style-cast]
23 | inline int unit_code(Unit unit) { return (int)unit; }
| ^~~~
| ---------
| static_cast<int> (unit)
sensor.h: In function 'std::size_t label_length(const std::string&)':
sensor.h:27:101: error: use of old-style cast to 'char*' [-Werror=old-style-cast]
27 | inline std::size_t label_length(const std::string& label) { return legacy_length((char*)label.c_str()); }
| ^
| -------
| const_cast<char*> ( )
sensor.h: In function 'uint32_t bits_of(float)':
sensor.h:30:70: error: use of old-style cast to 'uint32_t*' {aka 'unsigned int*'} [-Werror=old-style-cast]
30 | inline std::uint32_t bits_of(float value) { return *(std::uint32_t*)&value; }
| ^~~~~
| ----------------------
| reinterpret_cast<std::uint32_t*> (&value)
sensor.h: In function 'int humidity_of(Reading*)':
sensor.h:34:42: error: use of old-style cast to 'struct HumidReading*' [-Werror=old-style-cast]
34 | HumidReading* humid = (HumidReading*)reading;
| ^~~~~~~
| -
| static_cast< -
| > ( )
Text longer than 4,000 characters is cut here; run the example yourself to see all of it.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sensor_session.py
Replace each cast with the one that says what it does. GCC’s suggestions are a good start for the first three, but
not for the last two: its reinterpret_cast for bits_of() would keep the undefined behaviour, so include <bit>
and use std::bit_cast, and humidity_of() must return -1 for a plain Reading, which only a checked cast can tell.
Both compilers should then print 6 of 6 checks passed. Build once more with Clang and
-fsanitize=undefined,vptr, then change your dynamic_cast to a static_cast and run the tests again.
Then sort the casts below into correct, compiling but unsafe, undefined behaviour and not compiling, and apply the seven ideas of this lesson to cases it has not shown. The idea you know least well is asked about first, and each answer you pick tells you why it holds or where it fails:
Practice that adapts to you
Practise this lesson
19 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between static_cast and dynamic_cast?
Both can convert a pointer or reference to a base class into one to a derived class. static_cast does it when the program is compiled and checks only that the classes are related; if the object is not really of the derived class, the result is undefined behaviour. dynamic_cast checks the object’s real type while the program runs, using run-time type information, so it needs a class with at least one virtual function: a failed cast to a pointer gives nullptr, and a failed cast to a reference throws std::bad_cast. dynamic_cast therefore costs a run-time check, and it does not compile with -fno-rtti. static_cast also performs ordinary conversions such as double to int, which dynamic_cast cannot. A good answer adds that a virtual function often makes either downcast unnecessary.
The follow-ups usually test the edges of that answer: which form of dynamic_cast a bug calls for, and a cast between
two classes that only share a base. Decide both:
Checkpoint
Interview round: two follow-up questions
Two harder questions of this module start from this lesson: a game engine built without RTTI that must tell an enemy from other objects, and a legacy module with about 300 C-style casts:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your game engine builds with -fno-rtti, and a new feature needs to know whether a GameObject is an Enemy. Why does dynamic_cast not compile, and what are your options, with the cost of each?
-
Senior to staff
A legacy module contains about 300 C-style casts, and a bug was traced to one that silently became a reinterpret_cast. How would you clean up the casts safely, and how would you stop new ones?
Key takeaways
- Use the named cast that matches the conversion:
static_castfor ordinary conversions,dynamic_castfor checked downcasts,const_castonly to removeconst,reinterpret_castonly for bytes and addresses, andstd::bit_castfor bits as another type. - A failed
dynamic_castgives nullptr for a pointer and throwsstd::bad_castfor a reference. It needs RTTI, which-fno-rttiremoves along withtypeid; upcasts, casts tovoid*and exceptions still work. - A
static_castdowncast to the wrong class is undefined behaviour that may print a plausible wrong value. GCC’s-fsanitize=undefinedcatches it; Clang needs-fsanitize=vptras well. - Writing to an object defined
constthrough aconst_castpointer is undefined behaviour: the compilers kept using the original value. reinterpret_castnever adjusts a pointer for a base class at an offset;static_castdoes.- A C-style cast silently picks whichever cast compiles, even into a private base. Build with
-Wold-style-castand replace each one.
Terms in this lesson
- C-style cast
- The cast notation (T)x that C++ took over from C. It performs the first of const_cast, static_cast, static_cast then const_cast, reinterpret_cast and reinterpret_cast then const_cast that can express the conversion, even to a private base.
- downcast
- A conversion of a pointer or a reference to a base class into one to a derived class, such as a Shape* to a Circle*. dynamic_cast checks it while the program runs; static_cast trusts it, and a wrong one is undefined behaviour.
- named cast
- One of the four cast operators static_cast, dynamic_cast, const_cast and reinterpret_cast. Each may perform only its own kind of conversion, so the compiler rejects one that would do something else, which a C-style cast would allow.
- run-time type information (RTTI)
- The description of its own type that the compiler keeps for each class with virtual functions, which dynamic_cast's checks and typeid read while the program runs. With -fno-rtti GCC and Clang leave it out and reject those uses.
- upcast
- A conversion of a pointer or a reference to a derived class into one to a base class, such as a Circle* to a Shape*. For a public base it is implicit and always safe, and it needs no check while the program runs.
References
- Working Draft, Programming Languages: C++ (N4950): [expr.static.cast], [expr.dynamic.cast], [expr.typeid], [expr.const.cast], [expr.reinterpret.cast], [expr.cast], [bit.cast] (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: ES.48, avoid casts (Standard C++ Foundation)
- C++ Core Guidelines: ES.49, if you must use a cast, use a named cast (Standard C++ Foundation)
- C++ Core Guidelines: ES.50, don't cast away const (Standard C++ Foundation)
- C++ Core Guidelines: C.146, use dynamic_cast where class hierarchy navigation is unavoidable (Standard C++ Foundation)
- C++ Core Guidelines: C.147, use dynamic_cast to a reference type when failure to find the required class is an error (Standard C++ Foundation)
- C++ Core Guidelines: C.148, use dynamic_cast to a pointer type when failure is a valid alternative (Standard C++ Foundation)
- C++ Core Guidelines: C.153, prefer virtual function to casting (Standard C++ Foundation)
- Options Controlling C++ Dialect (Using the GNU Compiler Collection) (GNU Project)
- LLVM Coding Standards: Do not use RTTI or Exceptions (LLVM Project)
- Program Instrumentation Options (Using the GNU Compiler Collection) (GNU Project)
- UndefinedBehaviorSanitizer (Clang documentation) (LLVM Project)
- Itanium C++ ABI (run-time type information) (Itanium C++ ABI project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress