Programming Pandit

c/c++/c#/Javav/Python


Latest Update

Friday, October 23, 2026

Early and Late Binding, RTTI and Casting Operators

 


Early and Late Binding, RTTI and Casting Operators


Learning Objectives

After completing this module, students should be able to:

•      Explain early (static) and late (dynamic) binding and predict which one a given call uses.

•      Use run-time type identification: typeid and type_info.

•      Apply static_cast, dynamic_cast, const_cast and reinterpret_cast, and say why C-style casts are discouraged.

•      Perform safe downcasts with dynamic_cast using pointers and references.

•      Know which features depend on the C++ version or on compiler options.

 

1. Early and Late Binding

Definition: Binding is the connection between a function call and the function body that will be executed. Early (static, compile-time) binding makes this connection when the program is compiled. Late (dynamic, run-time) binding makes it while the program runs, using the real type of the object.

 

Title: Early binding fixes the function at compile time; late binding looks it up through the object at run time. - Description: Early binding fixes the function at compile time; late binding looks it up through the object at run time.

Figure 7.1: Early binding fixes the function at compile time; late binding looks it up through the object at run time.

Feature

Early binding

Late binding

Also called

Static, compile-time binding

Dynamic, run-time binding

Decided by

The static type of the expression

The dynamic type of the object

When

At compile time

At run time

Used for

Ordinary functions, overloaded functions, non-virtual members, static members, calls through objects

Virtual functions called through pointers or references

Speed

Fastest; can be inlined

Slightly slower (vtable lookup, no inlining)

Flexibility

Lower: the choice is fixed in the code

Higher: behaviour depends on the real object

Typical mechanism

Direct call

vptr and vtable (Module 5)

 

Program 7.1: The same pointer, calls that are bound early and late.

#include <iostream>

using namespace std;

 

class Base {

public:

    virtual void show() { cout << "Base::show (virtual)" << '\n'; }

    void display() { cout << "Base::display (non-virtual)" << '\n'; }

    virtual ~Base() = default;

};

 

class Derived : public Base {

public:

    void show() override { cout << "Derived::show (virtual)" << '\n'; }

    void display() { cout << "Derived::display (non-virtual)" << '\n'; }

};

 

int main()

{

    Derived d;

    Base *p = &d;

    Base &r = d;

 

    cout << "-- virtual function --\n";

    p->show();                   // late binding: Derived::show

    r.show();                    // late binding through a reference

 

    cout << "-- non-virtual function --\n";

    p->display();                // early binding: decided by the type Base*

    d.display();                 // early binding: decided by the type Derived

 

    cout << "-- suppressing dynamic dispatch --\n";

    p->Base::show();             // a qualified name forces Base::show

    return 0;

}

 

Output:

-- virtual function --

Derived::show (virtual)

Derived::show (virtual)

-- non-virtual function --

Base::display (non-virtual)

Derived::display (non-virtual)

-- suppressing dynamic dispatch --

Base::show (virtual)

 

•      p->show() and r.show() use late binding: show() is virtual and the object is a Derived.

•      p->display() uses early binding: display() is not virtual, so the type Base * decides. d.display() is bound early to the function of Derived.

•      p->Base::show() suppresses the dynamic dispatch: a call with a qualified name is always bound early.

 

Call

Binding

obj.f() on an object

Early (the exact type is known)

ptr->f() or ref.f() with f non-virtual

Early (static type)

ptr->f() or ref.f() with f virtual

Late

ptr->Base::f() (qualified name)

Early

Virtual call inside a constructor or destructor

Resolved to the class being constructed or destroyed (Module 6)

 

Note: Optimizing compilers may replace a virtual call by a direct call when they can prove the dynamic type (for example, a call on a local object). The program behaves as if late binding were used.

 

2. Run-Time Type Identification (RTTI)

Definition: RTTI is the C++ facility that lets a program find out the actual type of an object at run time. It consists of the typeid operator (with the class std::type_info) and the dynamic_cast operator (Section 4).

 

RTTI is useful when only a base pointer is available but the program must act on the real type, for example, to call a function that exists only in the derived class. It works only for polymorphic classes, that is, classes with at least one virtual function.

2.1 The typeid operator

#include <typeinfo>

typeid(expression)      // type of an expression

typeid(type)            // a type name

// both give a reference to a std::type_info object

 

Member of type_info

Purpose

operator==, operator!=

Compare two types

name()

A name for the type; the text is implementation-defined

before()

Order of two types (used for sorting types)

hash_code()

A hash value (C++11)

 

Program 7.2: typeid with polymorphic and non-polymorphic classes.

#include <iostream>

#include <typeinfo>

using namespace std;

 

class Base {

public:

    virtual ~Base() = default;               // polymorphic

};

class Derived : public Base {};

 

class P {};                                  // not polymorphic

class Q : public P {};

 

int main()

{

    cout << boolalpha;

 

    Base *b = new Derived;

    cout << "typeid(*b) == typeid(Derived): " << (typeid(*b) == typeid(Derived))

        << '\n';

    cout << "typeid(*b) == typeid(Base)   : " << (typeid(*b) == typeid(Base))

        << '\n';

    cout << "typeid(b)  == typeid(Base*)  : " << (typeid(b) == typeid(Base *))

        << '\n';

 

    // no virtual functions: static type only

    P *p = new Q;

    cout << "typeid(*p) == typeid(Q)      : " << (typeid(*p) == typeid(Q)) << '\n';

    cout << "typeid(*p) == typeid(P)      : " << (typeid(*p) == typeid(P)) << '\n';

 

    cout << "name of int     : " << typeid(int).name() << '\n';

    cout << "name of Derived : " << typeid(Derived).name() << '\n';

 

    Base *np = nullptr;

    try {

        cout << typeid(*np).name() << '\n';

    }

    catch (const bad_typeid &) {

        cout << "bad_typeid caught: typeid applied to a null pointer\n";

    }

    delete b;

    delete p;

    return 0;

}

 

Output (GCC; other compilers print different text for name()):

typeid(*b) == typeid(Derived): true

typeid(*b) == typeid(Base)   : false

typeid(b)  == typeid(Base*)  : true

typeid(*p) == typeid(Q)      : false

typeid(*p) == typeid(P)      : true

name of int     : i

name of Derived : 7Derived

bad_typeid caught: typeid applied to a null pointer

 

•      Base has a virtual destructor, so it is polymorphic: typeid(*b) gives the dynamic type, Derived, even though b is a Base *. Hence typeid(*b) == typeid(Derived) is true.

•      typeid(b) is the type of the pointer itself (Base *), a static type.

•      P has no virtual function; typeid(*p) then gives the static type P, even though the object is a Q.

•      The names printed by name() are compiler specific: GCC prints i and 7Derived, while Microsoft Visual C++ prints int and class Derived. Never compare these strings; compare type_info objects with ==.

•      typeid(*np) on a null pointer to a polymorphic class throws std::bad_typeid.

 

Version Note: typeid and dynamic_cast are part of C++ since the first standard (C++98); hash_code() and the helper class std::type_index (for use as a key in maps) were added in C++11.

RTTI can be switched off. Compilers have an option to disable it to save space (for example, GCC and Clang -fno-rtti). Then neither typeid nor dynamic_cast can be used:

 

error: cannot use ‘typeid’ with ‘-fno-rtti’

 

error: ‘dynamic_cast’ not permitted with ‘-fno-rtti’

 

3. Casting Operators

A cast converts a value or pointer from one type to another. The old C-style cast (T)expression does whatever it takes to make the conversion work: it may silently combine a downcast, a removal of const and a reinterpretation of bits. Such mistakes are hard to find because the cast is easy to type and hard to search for.

C++ therefore provides four named cast operators, each for one purpose, written cast_name<type>(expression):

Title: Which cast to use. - Description: Which cast to use.

Figure 7.2: Which cast to use.

Cast

Purpose

Checked?

static_cast

Conversions between related types: numeric types, enum, void *, upcasts and unchecked downcasts

At compile time only

dynamic_cast

Safe downcast or cross-cast in a polymorphic hierarchy

At run time (needs RTTI)

const_cast

Add or remove const or volatile

Compile time

reinterpret_cast

Reinterpret bits: unrelated pointers, pointer ↔ integer

Not checked at all

 

3.1 Problems of the C-style cast

Program 7.3: The C-style cast accepts a wrong downcast; the checked cast detects it.

#include <iostream>

using namespace std;

 

class Base { public: virtual ~Base() = default; };

class Derived : public Base { public: int x = 5; };

class Other : public Base { public: double y = 1.5; };

 

int main()

{

    Other o;

    Base *b = &o;

 

    /* The C-style cast compiles without any complaint ... */

    Derived *wrong = (Derived *)b;                        // really an Other object!

    // reading wrong->x would be undefined

    (void)wrong;

 

    /* ... whereas the checked cast detects the mistake. */

    Derived *safe = dynamic_cast<Derived *>(b);

    cout << "dynamic_cast result is " << (safe ? "valid" : "nullptr") << '\n';

 

    double d = 3.9;

    // same as static_cast<int>(d)

    int i = (int)d;

    cout << "(int)3.9 = " << i << '\n';

    return 0;

}

 

Output:

dynamic_cast result is nullptr

(int)3.9 = 3

 

The object is really an Other, but (Derived *)b compiles without complaint and gives a pointer that must not be used. The dynamic_cast returns nullptr, so the mistake can be handled. For the numeric conversion (int)d a C-style cast does the same as static_cast<int>(d), but the named cast is clearer and easier to find.

3.2 static_cast

static_cast<T>(e) performs conversions that the compiler can verify as meaningful from the types, such as double to int, an enumeration to an integer, void * back to the original pointer type, and a derived pointer to a base pointer. A downcast (base to derived) is accepted too, but not checked: the programmer must be sure that the object has the derived type.

Program 7.4: Typical uses of static_cast.

#include <iostream>

using namespace std;

 

class Base { public: virtual ~Base() = default; };

class Derived : public Base { public: void hello() const { cout

    << "Derived::hello\n"; } };

 

enum Color { RED, GREEN, BLUE };

 

int main()

{

    double price = 99.75;

    int amount = static_cast<int>(price);             // truncation, not rounding

    cout << "99.75 -> " << amount << '\n';

    cout << "-99.75 -> " << static_cast<int>(-99.75) << '\n';

 

    int a = 7, b = 2;

    cout << "a / b = " << a / b << ", with a cast: "

         << static_cast<double>(a) / b << '\n';

 

    cout << "BLUE as int = " << static_cast<int>(BLUE) << '\n';

    Color c = static_cast<Color>(1);

    cout << "1 as Color == GREEN: " << boolalpha << (c == GREEN) << '\n';

 

    int value = 42;

    void *vp = &value;                                // any object pointer -> void*

    int *ip = static_cast<int *>(vp);                 // back to the original type

    cout << "*ip = " << *ip << '\n';

 

    Derived d;

    Base *bp = static_cast<Base *>(&d);               // upcast (implicit would do)

    // unchecked downcast: valid here

    Derived *dp = static_cast<Derived *>(bp);

    dp->hello();

    return 0;

}

 

Output:

99.75 -> 99

-99.75 -> -99

a / b = 3, with a cast: 3.5

BLUE as int = 2

1 as Color == GREEN: true

*ip = 42

Derived::hello

 

•      Casting 99.75 to int truncates towards zero (99, and −99 for −99.75); it does not round.

•      a / b with two int values is integer division (3); casting one operand to double gives 3.5.

•      A pointer converted to void * and back with static_cast yields the original pointer.

 

A conversion between unrelated pointer types is rejected at compile time:

error: invalid ‘static_cast’ from type ‘A*’ to type ‘B*’

 

3.3 dynamic_cast

dynamic_cast<T>(e) converts a pointer or reference to a class within an inheritance hierarchy and checks at run time that the conversion is valid. It requires that the source type is polymorphic (has a virtual function).

Title: dynamic_cast checks the real type of the object before the conversion. - Description: dynamic_cast checks the real type of the object before the conversion.

Figure 7.3: dynamic_cast checks the real type of the object before the conversion.

Form

On success

On failure

Pointer: dynamic_cast<Derived *>(basePtr)

A valid Derived *

Returns a null pointer

Reference: dynamic_cast<Derived &>(baseRef)

A valid reference

Throws std::bad_cast

 

Program 7.5: A safe downcast with pointers, with references, and a cross-cast.

#include <iostream>

#include <typeinfo>

using namespace std;

 

class Base {

public:

    virtual ~Base() = default;

};

 

class Derived : public Base {

public:

    void show() const { cout << "Derived class function" << '\n'; }

};

 

class Other : public Base {};

 

/* two unrelated polymorphic classes combined in one object */

class Reader {

public:

    virtual ~Reader() = default;

    virtual void read() const { cout << "read

"; }

};

class Writer {

public:

    virtual ~Writer() = default;

    virtual void write() const { cout << "write

"; }

};

class File : public Reader, public Writer {};

 

int main()

{

    Derived d;

    Other o;

 

    /* pointer form: failure gives nullptr */

    Base *ptr = &d;

    Derived *dp = dynamic_cast<Derived *>(ptr);

    if (dp != nullptr)

        dp->show();

 

    ptr = &o;

    dp = dynamic_cast<Derived *>(ptr);

    if (dp == nullptr)

        cout << "Pointer cast failed safely: nullptr" << '\n';

 

    /* reference form: failure throws bad_cast */

    Base &ref = o;

    try {

        Derived &dr = dynamic_cast<Derived &>(ref);

        dr.show();

    }

    catch (const bad_cast &) {

        cout << "Reference cast failed: bad_cast thrown" << '\n';

    }

 

    /* cross-cast: from one base to a sibling base of the same object */

    File f;

    Reader *r = &f;

    Writer *w = dynamic_cast<Writer *>(r);            // sideways

    if (w != nullptr)

        w->write();

 

    /* dynamic_cast<void*> gives the address of the complete object */

    void *whole = dynamic_cast<void *>(w);

    cout << "Address of complete object: " << boolalpha

         << (whole == static_cast<void *>(&f)) << '\n';

    return 0;

}

 

Output:

Derived class function

Pointer cast failed safely: nullptr

Reference cast failed: bad_cast thrown

write

Address of complete object: true

 

1.      A Base * that really points to a Derived object converts successfully, and show(), which exists only in Derived, can be called.

2.      For an object of the sibling class Other the pointer cast gives nullptr: the failure is detected safely.

3.      There is no null reference, so the reference form throws bad_cast on failure; the program catches it.

4.      A cross-cast converts sideways from one base class (Reader) to another (Writer) of the same complete object, which static_cast cannot do.

5.      dynamic_cast<void *> returns the address of the most derived (complete) object.

 

Using dynamic_cast on a class that is not polymorphic is an error:

error: cannot ‘dynamic_cast’ ‘p’ (of type ‘class Base*’) to type ‘class Derived*’ (source type is not polymorphic)

 

Cost. dynamic_cast searches the type information at run time and is slower than static_cast. Frequent need for it often indicates that a virtual function would be a better design (Section 6).

typeid compared with dynamic_cast

 

typeid

dynamic_cast

Answers

Is the object of exactly this type?

Can the object be treated as this type (this type or a type derived from it)?

Result

A type_info reference

A converted pointer/reference (or null / exception)

Derived-of-derived objects

Not equal to the base type

Succeeds, since a derived object is-a base object

Use when

Exact type is needed

You want to use the object as that type

 

3.4 const_cast

const_cast<T>(e) adds or removes the const (or volatile) qualifier. It cannot change anything else. Its main legitimate use is to call an old function that does not use const although it only reads.

Program 7.6: Removing const from a pointer.

#include <iostream>

using namespace std;

 

/* an old C-style function that does not use 'const' although it only reads */

void legacyPrint(char *text)

{

    cout << "legacy: " << text << '\n';

}

 

int main()

{

    int value = 25;

    const int *ptr = &value;

    int *modifiable = const_cast<int *>(ptr);         // value itself is not const

    *modifiable = 40;

    cout << "Updated value: " << value << '\n';

 

    const char *message = "hello";

    // safe: the function only reads

    legacyPrint(const_cast<char *>(message));

 

    /* const int k = 25; *const_cast<int*>(&k) = 40;  -> undefined behaviour */

    return 0;

}

 

Output:

Updated value: 40

legacy: hello

 

•      ptr is a pointer to const int, but the variable value itself is not constant, so writing through the pointer made by const_cast is valid.

•      Passing const_cast<char *>(message) to a function that only reads the text is safe.

 

Important: Modifying an object that was defined as const through a pointer obtained with const_cast is undefined behaviour. const_cast removes the compiler’s protection; it does not make a constant object writable.

 

Without the cast the compiler rejects the conversion; and const_cast cannot change the type itself:

error: invalid conversion from ‘const int*’ to ‘int*’ [-fpermissive]

 

error: invalid ‘const_cast’ from type ‘const int*’ to type ‘double*’

 

3.5 reinterpret_cast

reinterpret_cast<T>(e) converts a pointer to an unrelated pointer type, or between a pointer and an integer, by reinterpreting the same bits. It does no check and no adjustment, so the result is often implementation-dependent and dangerous. It is used for low-level work such as hardware addresses, serialization and inspecting bytes.

Program 7.7: Pointer ↔ integer round trip and inspection of the bytes of an object.

#include <cstdint>

#include <iostream>

using namespace std;

 

int main()

{

    int value = 10;

    int *ptr = &value;

 

    uintptr_t address = reinterpret_cast<uintptr_t>(ptr);   // pointer -> integer

    // integer -> same pointer

    int *back = reinterpret_cast<int *>(address);

    cout << "Round trip gives the same pointer: " << boolalpha << (back == ptr)

        << '\n';

    cout << "Value through the pointer: " << *back << '\n';

 

    unsigned int number = 1;                                // look at the bytes

    const unsigned char *bytes = reinterpret_cast<const unsigned char *>(&number);

    cout << "First byte of 1: " << static_cast<int>(bytes[0])

        << " (1 means little-endian)\n";

    return 0;

}

 

Output (the last line depends on the machine; most desktop processors are little-endian):

Round trip gives the same pointer: true

Value through the pointer: 10

First byte of 1: 1 (1 means little-endian)

 

•      Converting a pointer to uintptr_t and back with reinterpret_cast gives the same pointer.

•      Looking at the bytes of an object through unsigned char * is always allowed; the first byte of the integer 1 is 1 on a little-endian machine and 0 on a big-endian machine.

 

Version Note: uintptr_t (header <cstdint>) is available from C++11.

C++20 adds std::bit_cast (header <bit>), a safer replacement for the usual uses of reinterpret_cast when only the bits of a value must be reinterpreted as another type of the same size.

 

Note: Never use reinterpret_cast to convert between pointers of related polymorphic classes: in multiple inheritance the correct pointer value may differ (Module 2), and reinterpret_cast does not adjust it. Use static_cast or dynamic_cast.

 

4. RTTI and Casting Combined

Program 7.8: Run-time polymorphism, typeid and dynamic_cast together.

#include <iostream>

#include <typeinfo>

using namespace std;

 

class Animal {

public:

    virtual void sound() { cout << "Animal sound" << '\n'; }

    virtual ~Animal() = default;

};

 

class Dog : public Animal {

public:

    void sound() override { cout << "Dog barks" << '\n'; }

    void fetch() { cout << "Dog fetches the ball" << '\n'; }

};

 

class Cat : public Animal {

public:

    void sound() override { cout << "Cat meows" << '\n'; }

};

 

void handle(Animal *a)

{

    a->sound();                                           // run-time polymorphism

    // run-time type identification

    if (typeid(*a) == typeid(Dog))

        cout << "Dynamic type is Dog" << '\n';

    if (Dog *d = dynamic_cast<Dog *>(a))                  // checked downcast

        d->fetch();

    else

        cout << "Not a Dog: no fetch()" << '\n';

}

 

int main()

{

    Dog dog;

    Cat cat;

    handle(&dog);

    cout << "---\n";

    handle(&cat);

    return 0;

}

 

Output:

Dog barks

Dynamic type is Dog

Dog fetches the ball

---

Cat meows

Not a Dog: no fetch()

 

•      a->sound() uses late binding (virtual function).

•      typeid(*a) == typeid(Dog) identifies the dynamic type, and the message is printed only for the dog.

•      dynamic_cast<Dog *>(a) lets the function call fetch(), which exists only in Dog; for a Cat the cast returns nullptr and the else branch runs. The declaration inside the if condition limits the pointer d to the if statement.

 

5. Comparison of the Four Casts

Feature

static_cast

dynamic_cast

const_cast

reinterpret_cast

Purpose

Related types

Safe downcast / cross-cast

Add/remove const

Reinterpret bits

Check

Compile time

Run time

Compile time

None

Needs polymorphic class

No

Yes

No

No

Failure

Undefined behaviour if wrong

nullptr or bad_cast

Undefined behaviour if object is really const

Undefined / implementation-defined

Run-time cost

None

Some

None

None

Safety

Medium

High

Low

Lowest

 

6. Design Guidelines

•      Prefer virtual functions to testing the type of an object: if you write if (typeid(*p) == typeid(X)) many times, add a virtual function instead.

•      Use dynamic_cast only when you really need the derived interface, and always check the result (pointer) or catch bad_cast (reference).

•      Do not use C-style casts in C++ code; the named casts say what you intend.

•      Avoid const_cast and reinterpret_cast except for the specific reasons given above.

•      Remember that a class must be polymorphic (have a virtual function, normally the destructor) for RTTI to work.

 

7. Unit 3 at a Glance: Which C++ Version?

Feature

C++98/03

C++11 or later

Inheritance, multiple inheritance, virtual base classes, friends

Yes

Yes

Virtual functions, pure virtual functions, abstract classes

Yes

Yes

Access declaration Base::member;

Deprecated (accepted with a warning)

Deprecated (accepted with a warning)

Using-declaration using Base::member;

Yes

Yes

override and final

No

Yes (C++11)

Default member initializers (int x = 0;)

No

Yes (C++11)

Inheriting constructors using Base::Base;

No

Yes (C++11)

= default special member functions

No

Yes (C++11)

unique_ptr, shared_ptr

No (only auto_ptr)

Yes (C++11); make_unique in C++14

typeid, type_info, dynamic_cast, the four named casts

Yes

Yes

type_info::hash_code(), type_index, uintptr_t

No

Yes (C++11)

std::bit_cast

No

C++20 only

 

Note: To check a program against a specific standard with GCC or Clang, compile with -std=c++98, -std=c++11, -std=c++14, -std=c++17 or -std=c++20, together with -Wall -Wextra.

 

8. Quick Revision

•      Early binding: decided at compile time from the static type (non-virtual functions, calls through objects, qualified names). Late binding: at run time through the vtable (virtual functions via pointer/reference).

•      RTTI: typeid gives the dynamic type for polymorphic classes (static type otherwise); name() is implementation-defined; nullptr dereference throws bad_typeid.

•      static_cast (related types, unchecked), dynamic_cast (checked, polymorphic; pointer → nullptr, reference → bad_cast), const_cast (const only), reinterpret_cast (bits).

•      Avoid C-style casts. Prefer virtual functions to type tests.

 

9. Important Examination Questions

Short-answer questions

1.      Define binding. Differentiate between early and late binding.

2.      Which calls are bound early and which late?

3.      What is RTTI? What is the use of the typeid operator?

4.      What is type_info? Is the string returned by name() portable?

5.      Why are named casts preferred over C-style casts?

6.      What does dynamic_cast return on failure for a pointer and for a reference?

7.      What is the difference between static_cast and dynamic_cast?

8.      When is const_cast used? When is it undefined behaviour?

 

Long-answer and programming questions

1.      Explain early and late binding with a program and a diagram.

2.      Explain RTTI with a program using typeid for a polymorphic base class.

3.      Explain the four casting operators with an example of each.

4.      Write a program that uses dynamic_cast to call a function that exists only in a derived class and handles the failure case.

5.      Write a program that shows bad_cast being thrown by a reference dynamic_cast.

6.      Compare the four casting operators in a table.

 

No comments:

Post a Comment