override 与 final¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++11 | 2/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-27 |
编译器支持:GCC 4.7+ · Clang 2.9+ · MSVC 14.0+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 在格式化器派生类上同时使用 final 与 override 固定继承契约 | ev-spdlog-override-final-formatter-001 |
| 用 final 标记异步 logger 禁止进一步派生 | ev-spdlog-final-async-logger-002 |
| 在装饰器派生类上用 override 覆写虚函数与析构函数 | ev-drogon-override-ratelimiter-003 |
本站证据页:ev-drogon-override-ratelimiter-003 · ev-spdlog-final-async-logger-002 · ev-spdlog-override-final-formatter-001
解决什么问题¶
覆写虚函数时,签名必须与基类完全一致。漏写 const、参数类型差一个引用、返回类型不兼容,都会让派生类函数变成新的重载而不是覆写。编译器不会报错,运行时却仍然调用基类版本——这类错误在大型继承体系里极难排查。另一方面,库作者有时需要明确禁止某个类被继续派生,或禁止某个虚函数被再次覆写,但语言原本没有表达手段。
override 让编译器检查“这个函数确实覆写了基类虚函数”,签名不匹配立即报错。final 放在类或虚函数后,禁止继续派生或覆写。两者都是上下文关键字,不改 ABI,也不产生运行期开销。标准提案为 N2928(override)与 N3206(final)。
基础用法¶
派生类函数末尾写 override;类名或函数声明末尾写 final。
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Square final : public Shape {
public:
explicit Square(double side) : side_(side) {}
double area() const override { return side_ * side_; }
private:
double side_;
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::unique_ptr<Shape>(new Square(3.0)));
std::cout << shapes.front()->area() << '\n';
}
输出 9。Square 用 final 禁止派生,area 用 override 让签名错误在编译期暴露。完整版本见 examples/cpp11/override_final.cpp。
进阶用法¶
override 只对虚函数合法。若基类函数不是虚函数,或参数、const、引用限定符不一致,编译器会直接报错。它同时替代了 virtual 关键字的位置:写了 override 就不必再写 virtual,语义仍然是虚函数。析构函数也可以标 override,尤其是多态基类的析构应声明为 virtual,派生类再用 override = default 明确覆写。
final 有两个位置。类名后的 final 禁止任何人继承该类;虚函数声明后的 final 禁止派生类继续覆写该函数,但类仍可被继承。两者都只影响编译期检查,不会改变对象布局或调用约定。
把 final 用在虚函数上还能给编译器更多优化空间:调用点若确定动态类型,编译器可以去除虚表间接调用。标准库和大型框架常用 final 封闭实现类,减少误用和优化障碍。注意 final 不是访问控制,它不阻止实例化或组合,只阻止继承与覆写。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| spdlog | include/spdlog/pattern_formatter-inl.h#L103-L113 |
派生格式化器同时标 final 与 override |
实现类不再扩展,覆写签名由编译器保证 |
| spdlog | include/spdlog/async_logger.h#L33-L34 |
异步 logger 类标 final |
公共 API 明确继承层级到此为止 |
| drogon | lib/inc/drogon/RateLimiter.h#L56-L69 |
装饰器类用 override 覆写虚函数与析构 |
基类签名变化时立即暴露,默认析构保持虚语义 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。三处都在生产路径上,覆盖实现类封闭、公共类封闭与装饰器覆写三种用途。
陷阱与边界¶
override 不能修饰非虚函数。 基类函数没有 virtual 时,派生类写 override 会编译失败;这通常说明基类接口设计有问题。
签名必须逐字匹配。 参数类型、const、引用限定符、异常规格都会影响覆写关系;返回类型只允许协变。override 的价值正是把这些差异变成编译错误。
final 类不能被继承。 测试替身、mock 和扩展点如果需要派生,不能把 final 加在类上;可以只对具体虚函数加 final。
析构函数也要虚。 通过基类指针删除派生对象时,基类析构必须是 virtual;override 只能检查覆写,不能弥补基类析构非虚的错误。
不要用 final 掩盖设计问题。 它适合封闭实现或明确不再扩展的接口;如果只是因为“暂时没人继承”就加 final,后续扩展会受阻。
ABI 与二进制兼容。 override 和 final 是编译期说明符,不改变布局;但给已有虚函数追加 final 可能影响内联与去虚化,跨版本库升级时仍需回归测试。
库联动¶
- 多态基类:接口类通常声明纯虚函数和虚析构,实现类用
override明确覆写。 - 标准库:
std::exception派生类、自定义迭代器与分配器常需要覆写虚函数,override可避免签名偏差。 - 测试框架:mock 类覆写接口时用
override,接口变更会在编译期暴露所有受影响替身。 - spdlog / drogon:日志格式化器与限流装饰器都用这两个说明符固定继承契约。
横向对比¶
| 方案 | 签名错误检查 | 禁止继续派生 | 运行期开销 | 主要限制 |
|---|---|---|---|---|
仅写 virtual |
否 | 否 | 有虚调用 | 拼写错误静默变成新函数 |
override |
是 | 否 | 无额外开销 | 只能用于虚函数 |
final |
否 | 是 | 可能利于优化 | 过度使用会阻碍扩展 |
override+final |
是 | 是 | 可能利于优化 | 同时限制扩展与覆写 |
| 纯虚接口 + 组合 | 编译期强制实现 | 视设计而定 | 有虚调用 | 继承层级固定,组合更灵活 |
跨标准演进:C++11 通过 N2928 引入 override、通过 N3206 引入 final(N3272 补充细节);二者此后未再变化,C++20 的 concept 与 requires 用于约束模板,不替代虚函数覆写检查。