Skip to content
academia.sh

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*>(&copy) == 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; &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.

// 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_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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close