C++98 到 C++14 迁移检查清单
老项目升级编译器后,"没指定 -std"往往意味着直接跳到 GNU++14。这不等于必须重写代码,而是需要按优先级处理一批具体问题。本文给出一份可执行的清单。
一、先固定编译基线
# 推荐起步组合:宽警告 + 废弃特性报错
g++ -std=gnu++14 -Wall -Wextra -Werror=deprecated-declarations -O2 -c foo.cpp
不要一上来就 -Werror 全开,否则老代码的警告会淹没真正的错误。策略是:
- 先让代码在
-std=gnu++14下编译通过; - 打开
-Werror=deprecated-declarations找出被废弃但仍在用的 API; - 逐步开启其他警告类,分批清理。
二、必改项:编译直接报错
1. 关键字冲突
C++11 引入的关键字不能再作标识符:auto、nullptr、constexpr、decltype、static_assert、thread_local、alignas、alignof、char16_t、char32_t、noexcept、override、final、using(原本就是关键字但语义扩展)。
// ❌ 老代码
int auto = 3;
// ✅
int autoCount = 3;
auto 是最常见的一个,通常出现在变量名或函数参数名上。
2. 模板尖括号解析
C++11 允许 >> 直接闭合嵌套模板,但老代码里写 > > 仍然合法。真正需要处理的是相反情况:某些宏或代码依赖 << 的旧解析。整体上这一项几乎无痛。
3. 移除的库设施
| 已移除/废弃 | 替代 |
|---|---|
std::auto_ptr | std::unique_ptr(C++11)、std::shared_ptr |
std::tr1::* | std::* |
std::unary_function / binary_function | 移除继承,或改用 lambda |
std::bind1st / bind2nd | lambda、std::bind |
std::mem_fun / mem_fun_ref | lambda、std::mem_fn |
register 关键字 | 直接删除 |
std::random_shuffle(C++17 移除) | std::shuffle + 显式引擎 |
std::strstream | std::stringstream |
注意 auto_ptr 在 C++14 下仍能编译但会警告,C++17 才彻底移除。迁移时按"拷贝即转移"的语义逐一确认目标类型。
4. 隐式窄化与指针转换
// ❌ C++11 起列表初始化禁止窄化
int x{3.14};
// ✅
int x = static_cast<int>(3.14);
花括号初始化在老代码中很少见,所以这条主要影响新写的代码。
三、建议项:编译警告
1. 旧式转型
(int)x → static_cast<int>(x)。批量处理时先只看不改,逐模块替换,避免误改 C 风格宏。
2. 未初始化变量
-Wmaybe-uninitialized 常常能在优化档位上暴露真实 bug。优先修,不要屏蔽。
3. 废弃的 STL 用法
vector.push_back(T(...))→emplace_back(...);map.insert(std::make_pair(k, v))→map.emplace(k, v);std::string与const char*混用时的隐式构造 → 显式std::string(...)。
4. 隐式 this 捕获
C++20 会禁止 [=] 隐式捕获 this。提前改掉可以让未来升级无痛:
// 现在
[=] { return member_; }
// 未来安全
[this] { return member_; } // 或显式 [=, this](C++20)
四、可选优化项
| 项目 | 收益 | 落地难度 |
|---|---|---|
| 迭代器循环改范围 for | 可读性 | 低 |
| 函数对象改 lambda | 代码量减少 | 低 |
NULL 改 nullptr | 消除重载歧义 | 极低 |
typedef 改 using | 可读性、支持模板别名 | 极低 |
| 裸指针改智能指针 | 消除泄漏 | 中(需线程安全评估) |
加 override | 编译期发现签名错误 | 极低 |
常量宏改 constexpr | 类型安全 | 低 |
五、override 与 final 的价值
struct Base { virtual void run(); };
struct Derived : Base {
void run() override; // 签名不匹配时编译报错
};
老代码里"以为覆盖了其实没覆盖"导致的 bug 极难排查,override 是零成本保险。批量加上后,编译器会一次性暴露所有签名错误。
六、分阶段执行流程
- 切基线:设
-std=gnu++14,先让项目编译通过(允许临时屏蔽警告); - 修必改项:关键字冲突、移除的库设施、
auto_ptr; - 清废弃 API:
-Werror=deprecated-declarations逐条替换; - 加安全网:批量补
override、nullptr、using; - 逐步现代化:范围 for、lambda、智能指针分批替换,每个模块独立提交;
- 回归验证:单测 + 集成测试 + 性能基准,确认无退化;
- 锁定标准:在构建脚本中显式写
-std=gnu++14,不再依赖默认值。
七、常见编译错误速查
| 报错关键词 | 原因 | 处理 |
|---|---|---|
'auto' does not name a type | auto 作变量名 | 重命名 |
'auto_ptr' is deprecated | 使用 auto_ptr | 换 unique_ptr |
narrowing conversion | 列表初始化窄化 | 显式转型 |
no matching function for call to 'bind1st' | 已移除 | 改 lambda |
'register' storage class specifier is deprecated | 使用了 register | 删除 |
invalid use of incomplete type | 前置声明后使用 | 补 include |
八、最后的建议
迁移的目标不是"用上所有新特性",而是"在新编译器上稳定运行,同时让代码更容易维护"。每一步改动都要能独立回滚。编译器是你的合作者:把警告当作待办清单,一条一条划掉。