23 / 80 · C++11 · 约 9 分钟
静态与动态多态 Polymorphism、切片与 RTTI
模板让调用按编译期类型组合,虚函数让同一基类接口在运行期选择实现。按值复制为基类会切片,丢失派生部分;RTTI 能安全查询活着的多态对象,但不能修复悬空指针或替代合理的接口设计。
选择发生在哪个阶段
静态多态常通过模板和重载实现:编译器针对实参类型选择并实例化操作,不要求类型共享基类。动态多态则通过虚函数和基类指针或引用,让同一个调用接口服务不同动态类型。前者有利于内联和类型组合,后者适合运行期选择实现及异构集合。
模板代码也可以调用虚函数,两种机制并不互斥。是否生成间接调用是优化与具体上下文的问题,不能把“模板一定快、虚函数一定慢”当定理。应先看类型何时确定、代码体积和接口稳定性,再测量热点。
引用保留对象,按值会切片
把 Derived 对象绑定到 Base&,引用仍指向原对象的基类子对象,虚调用可到达派生实现。写 Base b = derived; 则创建了新的完整 Base 对象,只复制基类部分,称为对象切片;新对象的动态类型就是 Base。
因此多态参数通常用引用或指针,多态拥有容器可保存 unique_ptr<Base>。切片不是未定义行为,也不是虚函数失效,而是你确实构造了另一个类型的对象。若接口不允许这种复制,可以把基类设计成抽象类型或限制其复制入口。
RTTI 查询与失败边界
对多态对象做运行期向下或交叉转换时,dynamic_cast 会检查继承关系;指针形式失败返回 nullptr,引用形式失败抛出 std::bad_cast。对多态 glvalue 使用 typeid 可以观察动态类型,但 typeid(pointer) 只查询指针类型,type_info::name 的字符串也不是可移植的业务标识。
示例分别验证模板调用、虚调用、切片和失败转换。RTTI 的前提是对象与生命周期都合法,拿悬空指针做 dynamic_cast 并不会得到安全的“已失效”提示。若每次业务操作都先向下转换,通常说明共同接口遗漏了真正需要的行为。
容易答错的地方
- vector<Base> 保存的是 Base 值,放入 Derived 不会保留完整派生对象;改用拥有指针或合适的值多态模型。
- dynamic_cast 的普通向上转换不要求源类型多态;需要运行期检查的向下、交叉转换才有相应多态要求。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .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);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-polymorphism.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
把 evaluate 的参数从 const T& 改为 T,并传入一个静态类型为 Base& 的表达式,会发生什么?
查看参考答案
模板按值推导会得到 T 为 Base,而不是运行期的 Derived。形参初始化复制 Base 子对象,形成切片,函数内 score() 返回一。保留 const T& 就不会创建新 Base 对象,动态调用仍返回九;模板参数推导本身不会依据运行期类型选择 Derived。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。