This post explores various techniques for determining the dynamic type of an object at runtime in C++ when dealing with inheritance hierarchies, along with best practices for using polymorphism and smart pointers.
We have a base class A
and derived classes B
and C
:
A (Base Class)
├── B (Derived Class)
└── C (Derived Class)
We want to be able to determine, at runtime, whether a pointer to A
is actually pointing to an object of type B
or C
.
dynamic_cast
(Recommended for Safe Downcasting)dynamic_cast
is the safest and most standard way to perform downcasting in a polymorphic hierarchy.
nullptr
(for pointer casts) or throws a std::bad_cast
exception (for reference casts).#include <iostream>
#include <memory>
class A {
public:
virtual ~A() {} // Important: A must have at least one virtual function
};
class B : public A {
public:
void bSpecific() { std::cout << "B specific\n"; }
};
class C : public A {
public:
void cSpecific() { std::cout << "C specific\n"; }
};
int main() {
std::unique_ptr<A> aPtr = std::make_unique<B>();
if (B* bPtr = dynamic_cast<B*>(aPtr.get())) {
std::cout << "aPtr is pointing to an object of type B\n";
bPtr->bSpecific();
} else {
std::cout << "aPtr is not pointing to an object of type B\n";
}
if (C* cPtr = dynamic_cast<C*>(aPtr.get())) {
std::cout << "aPtr is pointing to an object of type C\n";
cPtr->cSpecific();
} else {
std::cout << "aPtr is not pointing to an object of type C\n";
}
return 0;
}
Pros:
typeid
and type_info
(Less Common for Downcasting)typeid
returns a reference to a std::type_info
object, which contains type information.
#include <iostream>
#include <memory>
#include <typeinfo>
// ... (A, B, C classes as before)
int main() {
std::shared_ptr<A> aPtr = std::make_shared<C>();
if (typeid(*aPtr) == typeid(B)) {
std::cout << "aPtr is pointing to an object of type B\n";
} else if (typeid(*aPtr) == typeid(C)) {
std::cout << "aPtr is pointing to an object of type C\n";
} else {
std::cout << "aPtr is pointing to an object of type A or another derived type\n";
}
return 0;
}
Pros:
typeid
only identifies the type; it doesn't perform a cast. You'd need a separate static_cast
(unsafe) or dynamic_cast
for downcasting.typeid
comparisons can become cumbersome.Instead of explicit type checking, define virtual functions in the base class and override them in derived classes.
How it works:#include <iostream>
#include <memory>
#include <string>
class A {
public:
virtual std::string identify() = 0; // Pure virtual function
virtual ~A() {}
};
class B : public A {
public:
std::string identify() override { return "I am B"; }
};
class C : public A {
public:
std::string identify() override { return "I am C"; }
};
int main() {
std::unique_ptr<A> aPtr = std::make_unique<B>();
std::cout << aPtr->identify() << std::endl;
std::shared_ptr<A> aPtr2 = std::make_shared<C>();
std::cout << aPtr2->identify() << std::endl;
return 0;
}
Pros:
dynamic_cast
might be necessary.This approach combines an enum class
for type-safe identification with an unordered_map
to store string representations of the types.
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
enum class ClassType {
A,
B,
C
};
const std::unordered_map<ClassType, std::string> classTypeToString = {
{ClassType::A, "Class A"},
{ClassType::B, "Class B"},
{ClassType::C, "Class C"}
};
class A {
public:
virtual ClassType identify() = 0;
virtual ~A() {}
};
class B : public A {
public:
ClassType identify() override { return ClassType::B; }
};
class C : public A {
public:
ClassType identify() override { return ClassType::C; }
};
int main() {
std::unique_ptr<A> aPtr = std::make_unique<B>();
ClassType typeB = aPtr->identify();
std::cout << "Type: " << classTypeToString.at(typeB) << std::endl;
std::shared_ptr<A> aPtr2 = std::make_shared<C>();
ClassType typeC = aPtr2->identify();
std::cout << "Type: " << classTypeToString.at(typeC) << std::endl;
return 0;
}
Pros:
enum class
provides type safety.ClassType
enum and classTypeToString
map make the code self-documenting.std::unordered_map
provides fast lookups.ClassType
enum can be used for various purposes.dynamic_cast
.Manually adding a type identifier (e.g., an enum or string member) to the base class is generally not recommended.
Cons:dynamic_cast
when you need to know the exact type for reasons other than calling type-specific behavior. It's safe and standard.typeid
unless you have a specific reason to use it and understand its limitations for downcasting.Throughout these examples, we've used smart pointers (std::unique_ptr
and std::shared_ptr
) for automatic memory management.
std::unique_ptr
: Represents exclusive ownership. Use when you have a clear single owner.std::shared_ptr
: Represents shared ownership using reference counting. Use when multiple parts of your code need to share ownership.Using smart pointers is crucial for writing safe and robust modern C++ code. They prevent memory leaks and dangling pointers, making your programs more reliable.