C++ / a working model

23 / 80   ·   C++11   ·   约 9 分钟

静态与动态多态 Polymorphism、切片与 RTTI

先记住这句话

模板让调用按编译期类型组合,虚函数让同一基类接口在运行期选择实现。按值复制为基类会切片,丢失派生部分;RTTI 能安全查询活着的多态对象,但不能修复悬空指针或替代合理的接口设计。

本篇内容
  1. 选择发生在哪个阶段
  2. 引用保留对象,按值会切片
  3. RTTI 查询与失败边界
  4. 运行示例
  5. 动手练习

选择发生在哪个阶段

静态多态常通过模板和重载实现:编译器针对实参类型选择并实例化操作,不要求类型共享基类。动态多态则通过虚函数和基类指针或引用,让同一个调用接口服务不同动态类型。前者有利于内联和类型组合,后者适合运行期选择实现及异构集合。

模板代码也可以调用虚函数,两种机制并不互斥。是否生成间接调用是优化与具体上下文的问题,不能把“模板一定快、虚函数一定慢”当定理。应先看类型何时确定、代码体积和接口稳定性,再测量热点。

引用保留对象,按值会切片

把 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。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录