---
title: 'Type Conversion Operators'
source: 'https://academia.sh/en/courses/cpp-fundamentals/type-conversion-operators'
course: 'C++ Fundamentals and Object Design'
language: en
updated: '2026-09-26T03:02:46+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

# 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.

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.

```cpp
// 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;
}
```

```text
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.

```cpp
// 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*>(&copy) == static_cast<const void*>(&t)) ? "yes" : "no");
    return 0;
}
```

```text
measurer read called
same address no
```

**PS46.** `copy` is a new `Measurer` object independent of `t`; `&copy` 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.

```cpp
// 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;
}
```

```text
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:

```cpp
// 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;
}
```

```text
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.

```cpp
// 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;
}
```

```text
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_cast` does not construct a new
  object; it refers to the subobject inside `t` through a narrower type.
- The same `static_cast`, when the target is a value, constructs a new,
  independent object by slicing `t` — same operator, different class.
- `dynamic_cast` gives `nullptr` for a pointer, and `std::bad_cast` for a
  reference, on a failed downcast; in no case does it construct a new object.
- `const_cast` and `reinterpret_cast` also 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 to `t`'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.
