5.3 Casts and Functions

C++ Explicit Casts
Section titled “C++ Explicit Casts”Casting is one of the most troublesome operations in C/C++ because it instructs the compiler to bypass its type-checking system and trust the programmer. It should be used infrequently and only when no other solution is available. The two primary types of casting in object-oriented programming are upcasting and downcasting.
Upcasting and Downcasting
Section titled “Upcasting and Downcasting”- Upcasting is the conversion of a pointer or reference from a derived class to a base class. This is a natural and safe operation that happens automatically in C++, as a derived class is a more specialised version of its base class. Upcasting is fundamental to polymorphism, allowing you to treat an object of a derived type as if it were an object of a base type.
- Downcasting is the reverse: converting a pointer or reference from a base class to a derived class. This operation is inherently unsafe because a base class pointer might not actually be pointing to a derived class object. You must be cautious with downcasting; a failure to do so can lead to serious errors. C++ offers special casting operators like
dynamic_castto perform downcasting safely with a runtime check.

Figure 1. Upcasting vs. Downcasting
Think of the class inheritance hierarchy as a tree with the base class at the top. Moving up the tree to the base class is upcasting, while moving down to a more specific, derived class is downcasting.
Cross-casting is a lesser-known form of casting that involves converting a pointer or reference from one derived class to another sibling class that shares the same base class. This is also considered an unsafe operation and typically requires dynamic_cast for a safe runtime check.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Match the Casting Directions
Why is downcasting inherently considered an unsafe operation in C++?
The C++ Explicit Casts
Section titled “The C++ Explicit Casts”C++ introduced new, explicit casts to make casting safer, clearer, and more expressive by identifying the programmer’s intent. Unlike the older C-style cast (type), these new casts provide compile-time and even run-time checks, reducing errors and improving code readability.
static_cast: This cast is for all well-defined, “safe” conversions that the compiler can verify. It handles:- Automatic conversions: For example, converting an
intto afloat. - Narrowing conversions: Such as converting a
floatto anint(data loss may occur here). - Casting
void*: Converting avoid*back to its original pointer type. - Downcasting: Converting a base class pointer to a derived class pointer in cases where the inheritance hierarchy is non-polymorphic (i.e., has no virtual functions).
- Automatic conversions: For example, converting an
dynamic_cast: This is a type-safe downcast used exclusively for polymorphic classes (those with at least one virtual function). Its format isdynamic_cast<type>(expression). Thedynamic_castperforms a crucial run-time check to ensure the cast is valid. If the cast fails (e.g., the base class pointer doesn’t actually point to the specified derived class object), it returns anullptrfor a pointer or throws an exception for a reference. This check also helps in resolving ambiguous casts in multiple inheritance scenarios.const_cast: This cast is used specifically to add or removeconstorvolatilequalifiers from a variable. Its most common use is to removeconstfrom a variable to allow it to be passed to a function that doesn’t acceptconstarguments.reinterpret_cast: This is the most dangerous and least-used cast. It allows you to convert any pointer or integral type into any other pointer or integral type. It performs a simple bit-level reinterpretation without any type checking. It is intended for low-level, system-specific operations where you need to treat one type as another (e.g., converting a pointer to an integer for storage). Use it with extreme caution!
Here is an example of all four casts:
// Start reading from main() function to help make it easier to understand#include <iostream>#include <cstdint>using namespace std;
// Demo classes with a virtual base classclass Account {public: float balance; // for demonstration only! virtual ~Account() {}};
class Current : public Account {};
class Test {public: float x, y;};
// Demo Functionvoid someFunction(int& c) { c++; // c can be modified cout << "c has value " << c << "\n"; // will output 11}
int main() { float f = 200.6f; // narrowing conversion, but we have notified the compiler int x = static_cast<int>(f); // 1 cout << x << "\n";
Account* a = new Current; // upcast - cast derived to base // type-safe downcast - cast base to derived Current* c = dynamic_cast<Current*>(a); // 2
const int i = 10; // note i is const // someFunction(i); // is an error as someFunction could modify the value of i someFunction(*const_cast<int*>(&i)); // 3 // Allow i to be passed to the function but it will still remain at 10. cout << "i has value " << i << "\n"; // will still output 10
a = new Account; a->balance = 1000; // convert account address to uintptr_t (64-bit safe) uintptr_t addr = reinterpret_cast<uintptr_t>(a); // 4
// safe to convert uintptr_t address back into an Account pointer Account* b = reinterpret_cast<Account*>(addr); // 5 cout << "The balance of b is " << b->balance << "\n";
// could convert to any class regardless of inheritance - ok this time! (not safe) Current* cur = reinterpret_cast<Current*>(addr); // 6 cout << "The balance of cur is " << cur->balance << "\n";
// works, but definitely not safe this time! Test* test = reinterpret_cast<Test*>(addr); // 7 cout << "The value of the float x is " << test->x << " and float y is " << test->y << "\n";}The output of this code (64-bit compiler) is:
200c has value 11i has value 10The balance of b is 1000The balance of cur is 1000The value of the float x is -1.74315e-31 and float y is 4.59051e-41- The
static_casthere demonstrates the most common casting operation that we have previously performed using the C-style cast. In this case, we are simply converting afloatinto anintand notifying the compiler of our intention. Use astatic_castfor all such conversions. In this example 200.6 is converted to 200 as output above. - The
dynamic_casthere demonstrates downcasting an Account object to aCurrentobject. Adynamic_castmust be used when there are polymorphic base classes (those with virtual functions). In this example there is no output. - The
const_casthere takes theconst iand allows it to be passed by reference to a functionsomeFunction()that takes a non-constant parameter by reference. In this function we can treat the value just like a non-constant, but it has no impact on the original constant. As shown in the line directly above we would not have been allowed to pass this constant without theconst_cast. Note: Attempting to modify a variable that was originally declared asconstthrough aconst_castis Undefined Behaviour. While it may appear to work in some cases, the compiler is free to optimise based on the assumption that the constant never changes, which can lead to unpredictable results. - The
reinterpret_casthere converts the address of theAccountobject into auintptr_t. - The
reinterpret_castis used again, this time to convert auintptr_tinto a pointer to anAccountobject. This is dangerous as it is unchecked, but is perfectly appropriate in this case. The output shows a balance of 1000, so it worked correctly. On a 64-bit system a pointer is usually 64 bits wide, but alongis often only 32 bits (on Windows, for example). Casting a pointer tolongtherefore risks losing precision, and g++ warns (or errors) about it; therefore, the correct way in modern C++ is to usestd::uintptr_t(an unsigned integer type guaranteed to be large enough to hold a pointer) from<cstdint>. - The
reinterpret_castis then used to convert the sameuintptr_tvalue into aCurrentobject. Again it is fine in this case, but unchecked. - Finally, the
reinterpret_castis used to do a very unsafe operation of converting theuintptr_tvalue into the address of a randomTestobject. This works perfectly, but it is totally inappropriate as you can see in the output the int value of 1000 has been converted to afloat x, but the next block of random memory has also been assigned to the floaty. Modifyingywould cause unpredictable results, possibly crashing the application.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Applying Explicit C++ Casts
What is the result of a failed 'dynamic_cast' on a pointer type?
Is it safe to modify a variable that was originally declared as 'const' by using 'const_cast'?
When is it appropriate to use 'reinterpret_cast' in a C++ application?
Using explicit casts helps programmers identify potentially unsafe code because they clearly signal a break from normal type safety rules. This makes it easier to spot potential problems during debugging and code reviews. For example, explicit casts help debugging by providing:
- Explicit Intent: Unlike C-style casts, which are a “catch-all,” C++ casts (
static_cast,dynamic_cast,const_cast,reinterpret_cast) tell you why a cast is being performed. For example,const_castimmediately tells you that aconstqualifier is being removed, a risky operation. - Compile-time and Run-time Checks:
static_castprovides compile-time type checking, whiledynamic_castperforms a crucial run-time check for polymorphic types. This prevents silent errors that could lead to crashes or incorrect behaviour. If adynamic_castfails, it returns anullptr, which can be checked, allowing the program to handle the error gracefully instead of crashing. - Searching for “Danger Zones”: When debugging, a programmer can search for these specific cast keywords to quickly locate areas where the code is making an assumption about a type, which is a common source of bugs. The presence of a
reinterpret_castorconst_castoften indicates a section of code that requires careful scrutiny.
note:::[IEEE 745] IEEE 754 is the universal technical standard for representing floating-point numbers in computers, allowing them to store and calculate extremely large or extremely small real numbers efficiently. It achieves this by dividing a binary sequence (typically 32 bits for single precision or 64 bits for double precision) into three distinct components: a single sign bit that indicates whether the number is positive (0) or negative (1), a biased exponent that defines the number’s magnitude or scale, and a mantissa (or fraction) that holds the significant precision digits. By interpreting these three parts, the hardware calculates the actual numeric value, which provides a standardised way to handle a vast dynamic range of values. There are special bit patterns for cases like zero, infinity, and NaN (Not a Number). This format is used across virtually all modern computing architectures. :::
std::bit_cast: Safe Type-Punning (C++20) Advanced
Section titled “std::bit_cast: Safe Type-Punning (C++20) Advanced”The four casts above cover most scenarios, but one common pattern remained technically unsafe until C++20: type-punning (reading the raw bit representation of one type as another). This is a routine requirement in embedded and edge computing, for example inspecting the IEEE 754 binary encoding of a float, or packing sensor bytes into a typed struct.
Why reinterpret_cast is not the right tool here
The obvious approach is to take the address of the float, cast it to a uint32_t*, and dereference it, as follows:
float temperature = 23.5f;// Looks reasonable — but this is Undefined Behaviour (strict aliasing violation):uint32_t bits = *reinterpret_cast<uint32_t*>(&temperature);C++‘s strict aliasing rule forbids reading the bytes of a float through a uint32_t*. In practice most compilers generate the expected code, but they are equally entitled to optimise the load away entirely, because the standard permits them to assume the two pointers never alias. The memcpy workaround is defined behaviour but verbose and not constexpr.
We can use std::bit_cast
std::bit_cast<To>(from), added in <bit> (C++20), copies the exact bit pattern of from into a new value of type To. It is well-defined, one line, and constexpr:
#include <bit>#include <cstdint>#include <iostream>using namespace std;
int main() { float temperature = 23.5f; // Inspect the raw IEEE 754 bit pattern — well defined and constexpr uint32_t bits = bit_cast<uint32_t>(temperature); cout << hex << "Float " << temperature << " as bits: 0x" << bits << "\n";
// Round-trip: recover the float from its bit pattern float recovered = bit_cast<float>(bits); cout << dec << "Recovered: " << recovered << "\n";}This gives the output:
Float 23.5 as bits: 0x41bc0000Recovered: 23.5bit_cast enforces two constraints at compile time that reinterpret_cast ignores entirely:
sizeof(To)must equalsizeof(From)(a size mismatch is a compiler error).- Both types must be trivially copyable (types with non-trivial constructors or destructors (e.g.,
std::string) are rejected). Because it isconstexpr,bit_castcan also appear insideconstexprfunctions, whichreinterpret_castcannot:
constexpr uint32_t float_bits(float f) { return bit_cast<uint32_t>(f);}constexpr uint32_t one = float_bits(1.0f); // 0x3f800000 — evaluated at compile timeUse bit_cast whenever you need to reinterpret the bit pattern of a value as it gives you exactly what reinterpret_cast appears to give you, but with defined behaviour and compile-time verification.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Why is using reinterpret_cast to inspect the bit pattern of a float through a uint32_t pointer considered Undefined Behaviour in C++?
Which of the following would cause std::bit_cast to produce a compile-time error?
Functions and Methods in C++
Section titled “Functions and Methods in C++”Default Arguments
Section titled “Default Arguments”Parameters can be assigned default argument values, which must appear at the end of the parameter list. When a caller omits an argument for such a parameter, the default value is used automatically.
Example: Default Arguments
// amount has a default argument of 1void decrement(int &aValue, int amount = 1) { aValue = aValue - amount;}
// Usage examplesint x = 10;decrement(x, 3); // x now has the value 7decrement(x); // x now has the value 6 (amount defaults to 1)This example demonstrates that an argument for the amount parameter can be omitted, in which case the default value 1 is used. The full source code is provided in DefaultParameters.cpp.
Rules for Default Arguments in C++:
- Default argument values must be specified in the function declaration or definition, but not both.
- Parameters with default arguments must appear at the end of the parameter list.
- If an argument is supplied for a parameter with a default, arguments must also be supplied for all preceding parameters.
- Default arguments are evaluated at the call site, not at the point of definition.
- Function overloading and default arguments can sometimes conflict; be careful to avoid ambiguity.
Rule of thumb: Default arguments are good practice for small, self-contained functions where the default behaviour is obvious. For complex APIs, prefer clear overloads instead.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Defining Default Arguments
Which of the following is a strict rule for using default arguments in C++ functions?
Constant Parameters
It is often good practice to prevent modification of function parameters where this is not intended. This is particularly important when parameters are passed by reference, as it avoids accidental changes to the original variable. In such cases, the const keyword should be used.
Example: Constant Parameters
// Example with a constant parametervoid decrement(const int &aValue, int amount = 1) { aValue = aValue - amount; // ERROR: cannot modify a constant parameter}In this version, aValue is declared as a constant reference. Any attempt to modify it will cause a compile-time error. The full source code is available in ConstantParameters.cpp.
Of course, a decrement function that cannot decrement would be pointless, but this example illustrates how const protects parameters from modification.
Const-Qualified Methods \
The const keyword can also be applied to class methods to indicate that they do not modify the calling object. This ensures const safety, even if the method’s implementation does not explicitly change the object’s state.
Example: Const-Qualified Methods (constQualified.cpp):
class Number {private: int value;public: Number(int value) : value(value) {}
void a() const {} // const-qualified method void b() {} // non-const method void c() const; // const-qualified declaration};
void Number::c() const {} // must also include const here
int main() { Number n(5); // mutable object n.b(); // allowed
const Number m(10); // const-qualified object m.a(); // allowed (const method) m.b(); // error: b() is not const-qualified return 0;}Key rules:
- Const and non-const methods can be called by non-const objects (mutable instances).
- Only const-qualified methods can be called by const objects.
- Even if a non-const method does not modify the object, it cannot be called by a const object unless explicitly marked
const.
🧩Knowledge Check
Section titled “🧩Knowledge Check”Match Const Safety Concepts
If an object is declared as 'const', what are the restrictions on calling its member methods?
© 2026 Derek Molloy, Dublin City University. All rights reserved.