Lesson 14 / 14
Type Conversion Operators
The measurement of whether static_cast, dynamic_cast, const_cast, and reinterpret_cast access a reference as a part of an existing object or as a copy in a new one, depending on the target type.
Published Updated
In this lesson
- Measures that converting to a base reference with static_cast does not construct a new object, but refers to the same address as a part
- Shows that the same static_cast, when converting to a target value type, constructs a new copy by slicing the object
- Verifies that dynamic_cast gives nullptr for a pointer on a failed downcast, and std::bad_cast for a reference
- Shows that const_cast and reinterpret_cast also do not construct a new object, but open a different access path to the existing object
Contents
The previous lesson showed that a using declaration and a namespace alias do not
construct a new symbol, but open a local access path to an existing symbol — the
measurement was at the namespace boundary. This lesson lowers the boundary one more
level: type conversion operators. When we convert a Thermometer object with
static_cast to the base Measurer, the question stays the same — which object
did t refer to, and which object does the conversion give? The answer does not
depend on the operator’s name but on the target type: the same static_cast
refers to the subobject inside t when the target is a reference; it constructs a
brand new object when the target is a value.
A Reference Converted to the Base
Thermometer derives from Measurer. When we convert t to const Measurer&
with static_cast, the compiler does not construct a new object; it produces a
reference that views the Measurer subobject inside t through a narrower type.
This reference’s address stays within the same range as t’s address — because
the base class subobject is a part of the derived object.
// base-reference.cpp -- does static_cast reference conversion construct a new object
#include <cstdio>
class Measurer {
public:
virtual ~Measurer() = default;
void read() const { std::printf("measurer read called\n"); }
};
class Thermometer : public Measurer {
public:
int degree() const { return degree_; }
private:
int degree_ = 20;
};
int main() {
Thermometer t;
const Measurer& base = static_cast<const Measurer&>(t);
base.read();
std::printf("same address %s\n",
(static_cast<const void*>(&base) == static_cast<const void*>(&t)) ? "yes" : "no");
return 0;
}
measurer read called same address yes
PS45. base refers to the Measurer subobject inside t; the read() call
does not operate on a new object, but on t‘s own part. &base and &t coming
out at the same address range confirms this — the conversion did not call a
constructor, it only narrowed the type viewing t. This expression’s class is
part.
Value Conversion: Same Operator, Different Class
Now convert the same t, this time with the target being a value instead
of a reference: static_cast<Measurer>(t). The operator name is the same —
static_cast — but the target type is no longer a reference, it is Measurer
itself. Here the compiler copies t’s Measurer subobject and constructs a
brand new, independent Measurer object.
// value-conversion.cpp -- does static_cast construct a new object with a value target
#include <cstdio>
class Measurer {
public:
virtual ~Measurer() = default;
void read() const { std::printf("measurer read called\n"); }
};
class Thermometer : public Measurer {
public:
int degree() const { return degree_; }
private:
int degree_ = 20;
};
int main() {
Thermometer t;
Measurer copy = static_cast<Measurer>(t);
copy.read();
std::printf("same address %s\n",
(static_cast<const void*>(©) == static_cast<const void*>(&t)) ? "yes" : "no");
return 0;
}
measurer read called same address no
PS46. copy is a new Measurer object independent of t; © and &t
come out at different addresses. Despite being the same operator as
static_cast<const Measurer&>(t) in the previous section, this is a completely
different result: here t‘s part specific to Thermometer (degree_) is
discarded, and only the Measurer part is copied — this is object slicing.
This expression’s class is copy.
Dynamic Conversion: A Decision Based on the Real Type
static_cast trusts the target type blindly; it decides at compile time.
dynamic_cast, however, looks at the object’s real type at run time — if the
object behind the base reference is not actually of the requested derived type,
the conversion fails. Failure gives nullptr for a pointer target; it throws an
exception for a reference target.
// dynamic-conversion.cpp -- what does dynamic_cast give on a failed downcast
#include <cstdio>
#include <typeinfo>
class Measurer {
public:
virtual ~Measurer() = default;
};
class Thermometer : public Measurer {
public:
int degree() const { return degree_; }
private:
int degree_ = 20;
};
class Barometer : public Measurer {
public:
int pressure() const { return pressure_; }
private:
int pressure_ = 1013;
};
int main() {
Thermometer t;
Measurer& base = t;
Barometer* p = dynamic_cast<Barometer*>(&base);
std::printf("pointer conversion succeeded %s\n", p ? "yes" : "no");
try {
Barometer& b = dynamic_cast<Barometer&>(base);
(void)b;
std::printf("reference conversion caught %s\n", "no");
} catch (const std::bad_cast&) {
std::printf("reference conversion caught %s\n", "yes");
}
return 0;
}
pointer conversion succeeded no reference conversion caught yes
PS47. The real object behind base is a Thermometer, not a Barometer.
dynamic_cast<Barometer*> notices this at run time and returns nullptr — it
does not throw an exception, it just says “not here.” The same failure throws
std::bad_cast for a reference target; here only the fact that it was caught is
printed, not the text of what() — the exact form of the message depends on the
library implementation, which is not what this lesson measures. Since p is
nullptr, there is no constructed object here; a failed dynamic_cast gives
neither a copy nor a part, nothing remains.
Removing Constness: const_cast
const_cast changes only the constness qualifier of a reference or pointer; the
target type stays the same class, no new object is constructed. The review
function below removes constness once to mark an object it received through a
const reference:
// const-cast.cpp -- does removing constness construct a new object
#include <cstdio>
class Measurer {
public:
void mark() { marked_ = true; }
bool marked() const { return marked_; }
private:
bool marked_ = false;
};
static void review(const Measurer& m) {
const_cast<Measurer&>(m).mark();
}
int main() {
Measurer m;
review(m);
std::printf("marked %s\n", m.marked() ? "yes" : "no");
return 0;
}
marked yes
PS48. const_cast<Measurer&>(m) gives a reference that views m itself,
only with the constness qualifier removed — neither copy nor part, m’s
self. The condition for this being safe is that m’s actual object is
really not defined as const; the m in main is not constant, so the call to
mark() does not enter undefined behavior. The same call on an actually const
object would be undefined behavior — this lesson neither writes nor runs such a
call.
Reinterpreting the Bit Pattern: reinterpret_cast
reinterpret_cast is the loosest conversion: no relationship is required
between the target type and the source type, only the same bit pattern is read
as a different type. The only guarantee is that converting back to the same type
yields a value equal to the original — the intermediate value itself carries no
portable meaning, so this lesson does not print it, it only measures the
round-trip equality.
// reinterpretation.cpp -- reinterpret_cast round-trip equality
#include <cstdio>
#include <cstdint>
int main() {
int value = 42;
int* p = &value;
std::uintptr_t numeric = reinterpret_cast<std::uintptr_t>(p);
int* back = reinterpret_cast<int*>(numeric);
std::printf("round trip equal %s\n", (back == p) ? "yes" : "no");
return 0;
}
round trip equal yes
PS49. reinterpret_cast also does not construct a new object; it reads the
bit pattern carried by p first as an integer, then again as a pointer.
back == p equality shows that the same address comes back out — but this
guarantee holds only for a round trip that returns to the same type; the number
carried by the intermediate numeric value itself is platform-dependent and is
not this lesson’s subject.
So which of these four operators does a C-style cast — (Measurer)t,
(Measurer&)t, and the like — resolve to? The compiler tries them in order:
first const_cast, then static_cast, then static_cast combined with
const_cast, and finally reinterpret_cast. The first one that makes sense is
chosen — so which operator a C-style cast corresponds to can be inferred by
looking at the relationship between the target type and the source type, but
seeing this while reading is harder than writing static_cast or
dynamic_cast. This lesson does not use a C-style cast; it names it only to
show which operator it resolves to.
The Bounding Measurement
This lesson showed that the name of none of the four conversion operators
determines the class — what determines it is the target type. The same
static_cast refers to the subobject inside t (part) when the target is a
reference, and constructs a new object (copy) when the target is a value. This
lesson measured these two separately; guessing the class by looking at the
operator’s name was never done in this lesson.
Downcasting an object that is not actually of that type with an incorrect
static_cast — for example converting the object behind base, which is
actually a Thermometer, with static_cast<Barometer&> — is undefined
behavior: the compiler does not prevent it, and no check is made at run time
either. This lesson neither wrote nor ran such a call; the path this lesson
shows for safe downcasting is dynamic_cast. The platform-dependent
interpretation of the intermediate values reinterpret_cast produces, the
undefined behavior of const_cast on an actually const object, and the
compiler-specific details of a C-style cast’s resolution order are also outside
this lesson’s scope.
Summary
- Converting to a base reference with
static_castdoes not construct a new object; it refers to the subobject insidetthrough a narrower type. - The same
static_cast, when the target is a value, constructs a new, independent object by slicingt— same operator, different class. dynamic_castgivesnullptrfor a pointer, andstd::bad_castfor a reference, on a failed downcast; in no case does it construct a new object.const_castandreinterpret_castalso do not construct a new object; they open a different access path to the existing object.- The expression this lesson measures — the base reference obtained with
static_cast<const Measurer&>(t)— is of class part: it refers tot’s own address, through a narrow type.
Course Wrap-Up
This lesson was the last lesson of the C++ Fundamentals and Object Design course. Throughout the course, every lesson asked the same question at a different boundary: which object does an expression work with — itself, a copy of it, a part of it, or an unnamed temporary?
| Lesson | Measured expression | Which object | Bounding measurement |
|---|---|---|---|
| Differences Between C and C++ | struct passed by value |
copy | The call site’s class does not say; the decision is in the parameter type |
| Classes and Access Control | this of the member function |
self | private protects the boundary of the class, not the object |
| Constructors and Destructors | argument constructed from an int |
temporary | The lifetime of a temporary bound to a const reference is extended |
| RAII | member left by the guard |
part | The dangling of a guard holding a reference to a resource was only narrated |
| Copying and Assignment | deep-copied buffer |
copy | The depth of the copy is read only from the copy constructor’s body |
| References | struct passed by reference |
self | A const reference parameter can also bind to a temporary |
| Operator Overloading | result of a + b |
temporary | In a + b + c, only the intermediate result is temporary; the final result is constructed directly in sum |
| Inheritance | function of the base class |
part | Limited to single, non-virtual inheritance |
| Virtual Functions and Vtables | virtual call from a base reference |
self | Assignment by value and a non-virtual destructor are out of scope |
| Abstract Classes and Interfaces | call through an interface |
self | Multiple interface inheritance was left to the next lesson |
| Multiple Inheritance and the Diamond Problem | subobject of the virtual base |
part | The indirection cost of accessing the virtual base was not measured |
| Exceptions | exception caught by value |
copy | catch(...) and throwing inside noexcept are out of scope |
| Namespaces and Code Organization | object referred to by a using declaration |
self | ADL and #include mechanics are out of scope |
| Type Conversion Operators | reference converted to the base |
part | An incorrect static_cast downcast was only narrated, not run |
5 self · 3 copy · 4 part · 2 temporary; 9 existing, 5 new
Across fourteen lessons the same question was asked at fourteen different boundaries; the answer was always given by looking at what the compiler actually constructs, never by the name of the operator or the syntax. The next course carries this question one step further: when an object gives up the resource it owns, when it moves a value instead of copying it, and how the standard library uses this move.
To keep your progress and take notes, Log in
My notes
Log in to take notes.