23 / 163 · C++11 · 9 min
Static and Dynamic Polymorphism, Slicing, and RTTI
Templates compose calls from compile-time types; virtual functions let one base interface choose an implementation at run time. Copying by value into a base object slices away the derived part. RTTI can query a live polymorphic object safely, but it cannot repair a dangling pointer or replace a sound interface design.
In this lesson
When the choice is made
Static polymorphism is commonly implemented with templates and overloading. The compiler selects and instantiates an operation for the argument types; those types are not required to share a base class. Dynamic polymorphism instead uses virtual functions together with a base-class pointer or reference, so that the same call interface can serve different dynamic types. The former favors inlining and type composition. The latter is a better fit when an implementation must be chosen at run time, or when a heterogeneous collection must sit behind one interface.
Template code can also call virtual functions. The two mechanisms are not mutually exclusive. Whether the compiler emits an indirect call is a matter of optimization and of the concrete context. Do not treat “templates are always fast, virtual functions are always slow” as a theorem. First look at when the type is known, at code size, and at interface stability; then measure the hot path.
A reference keeps the object; a value copy slices
Binding a Derived object to a Base& still refers to the base subobject of the original object, so a virtual call can reach the derived implementation. Writing Base b = derived; creates a new, complete Base object and copies only the base part. That is object slicing; the dynamic type of the new object is Base.
Polymorphic parameters therefore usually take a reference or a pointer. A container that owns polymorphic objects can store unique_ptr<Base>. Slicing is not undefined behavior, and it is not virtual functions failing. You really constructed an object of another type. If the interface must not allow that copy, design the base as an abstract type or restrict its copy operations.
RTTI queries and failure boundaries
When you perform a run-time downcast or cross-cast on a polymorphic object, dynamic_cast checks the inheritance relationship. The pointer form returns nullptr on failure; the reference form throws std::bad_cast. Using typeid on a polymorphic glvalue observes the dynamic type, but typeid(pointer) queries only the pointer type, and the string from type_info::name is not a portable business identifier.
The example separately verifies a template call, a virtual call, slicing, and a failed conversion. RTTI presupposes a valid object and a valid lifetime. Performing dynamic_cast on a dangling pointer does not produce a safe “already invalid” indication. If every business operation first downcasts, the common interface is usually missing the behavior you actually need.
Pitfalls
- vector<Base> stores Base values; inserting a Derived does not keep the complete derived object. Use owning pointers or a suitable value-polymorphism model instead.
- An ordinary upcast with dynamic_cast does not require the source type to be polymorphic; only downcasts and cross-casts that need a run-time check have that polymorphism requirement.
Run an example
Minimum C++11 · complete program · Download .cpp
#include <cassert>
#include <typeinfo>
struct Base {
virtual int score() const { return 1; }
virtual ~Base() = default;
};
struct Derived : Base {
int score() const override { return 9; }
};
struct StaticScore {
int score() const { return 4; }
};
template<class T>
int evaluate(const T& object) { return object.score(); }
bool is_derived(const Base& object) {
return dynamic_cast<const Derived*>(&object) != nullptr;
}
int main() {
StaticScore fixed;
assert(evaluate(fixed) == 4);
Derived derived;
Base& reference = derived;
Base sliced = derived;
assert(evaluate(reference) == 9);
assert(sliced.score() == 1);
assert(dynamic_cast<Derived*>(&reference) == &derived);
assert(is_derived(reference));
assert(!is_derived(sliced));
assert(typeid(reference) == typeid(Derived));
bool failed = false;
try {
(void)dynamic_cast<Derived&>(sliced);
} catch (const std::bad_cast&) {
failed = true;
}
assert(failed);
}
Compile locally
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-polymorphism.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
If you change evaluate's parameter from const T& to T and pass an expression whose static type is Base&, what happens?
Show a reference answer
By-value template deduction yields T as Base, not the run-time Derived. Initializing the parameter copies the Base subobject, which slices, so score() inside the function returns one. Keeping const T& does not create a new Base object, so the dynamic call still returns nine. Template argument deduction itself does not select Derived from the run-time type.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.