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

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):

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