目录
1696 字
8 分钟
编译器报错大赏:一句话能写几屏

一、六行代码,七十八行报错#

先看一份很普通的 C++:

#include <algorithm>
#include <vector>
struct User { int age; };
int main() {
std::vector<User> users{{20}, {30}};
std::sort(users.begin(), users.end());
}

六行有效代码,意图清楚:把用户按年龄排序。编译一下:

Terminal window
$ g++ -std=c++20 -c sort.cpp | wc -l
78
$ clang++ -std=c++20 -c sort.cpp | wc -l
220

同一份代码,同一个错误。区别是 Clang 更「贴心」,多送了 142 行。

真正有用的信息其实只有一句:

predefined_ops.h:45:23: error: no match for 'operator<' (operand types are 'User' and 'User')

它既不在第一行,也不在末行。它藏在输出中间,前后夹着一堆 required from 和 note。编译器其实早就知道答案了:User 没有定义 operator<,所以 std::sort 不知道谁排在谁前面。它只是先带你把 std::sort 的内部装修看了一遍。

二、报错是倒着打印的调用栈#

把 GCC 那份输出按行拆开,大致是三类:

行的样子它在说什么要不要细读
error: ...真正失败的那一步先读
required from here / in instantiation of谁把这段代码拖进来的找到自己的文件
note: candidate: ...编译器考虑过哪些重载或约束解释「为什么没选中」

关键在于:编译器不是从上往下讲故事,而是从事故现场往回追溯。底部是事故本身,往上依次是「谁调用了它」,一直追到你的 main。所以在模板报错里,自己的代码往往只出现在一条 required from 里:

sort.cpp:6:14: required from here

其余几十行,是标准库的装修记录。

读长报错的第一招:不要从末行读,先搜你的文件名。 找到那条 required from here,就能定位到真正该改的地方;note: candidate 则告诉你,编译器试过哪些别的写法,以及为什么都不合适。

三、为什么模板能把一句话撑成四屏#

模板是按需实例化的。std::sort 内部会一路走到 __introsort_loop、__insertion_sort,再调用 _Iter_less_iter::operator();错误发生在这一层,而这一层看到的类型已经被展开成了:

__gnu_cxx::__normal_iterator<User*, std::vector<User>>

每经过一层实例化,编译器就多记一条 note。GCC 的 -ftemplate-backtrace-limit 默认只打印 10 条模板实例化 note,超过的部分会折叠成一句:

[ skipping 2 instantiation contexts, use -ftemplate-backtrace-limit=0 to disable ]

这不是编译器在凑字数,而是它在把自己推导的每一步摊开给你看。想让它少说点,可以调小上限:

Terminal window
g++ -std=c++20 -ftemplate-backtrace-limit=3 -c sort.cpp

想让它把折叠的部分也吐出来,就按它自己的提示设为 0。长度由你控制,前提是你知道这个旋钮在哪。

四、三种语言的报错性格#

C++:事故报告附完整通话记录#

GCC 和 Clang 都是倒序栈,但排版口味不同。这次 Clang 反而更长,因为它更爱把每一条 note: in instantiation of ... requested here 完整展开。同一个错误,一个 78 行,一个 220 行,说明「报错友好」和「报错短」并不是同一件事。

Rust:先给结论,再给修法#

Rust 的报错风格大致是这样(示意):

error[E0277]: the trait bound `User: Ord` is not satisfied
--> src/main.rs:5:14
|
5 | users.sort();
| ^^^^ the trait `Ord` is not implemented for `User`
|
help: consider deriving `Ord`

它把「哪里错、为什么错、怎么改」排成固定顺序,错误码还能单独查文档。库作者也可以用 #[diagnostic::on_unimplemented] 给自定义 trait 写更具体的提示,让用户不必去猜实现者的意图。代价是错误信息通常更短,有时会省略掉一部分推导细节。

TypeScript:类型展开,然后省略#

TypeScript 的经典报错是「这个类型不能赋给那个类型」,随后把对象字面量的每个属性列出来。类型一深,它就开始省:

Type '{ id: string; name: string; ... 12 more ... }' is not assignable to type 'User'.

noErrorTruncation 可以关掉这个省略号,代价是终端可能被一整页类型定义刷屏。省流量还是省脑细胞,得自己选。

五、把报错读短的三招#

第一招:从开头那条 error: 读起。 note 是补充说明,不是新的错误;编译器的 compilation terminated 只是收尾。一个错误后面跟着十条 note,通常仍然只是一个错误。

第二招:搜自己的文件名。 模板报错里,标准库的路径会反复出现,你的文件往往只出现一两次。那一两次就是锚点。找到 required from here,再往上看它要求的是什么类型。

第三招:把长表达式拆成具名中间变量。 一行里套三个泛型调用,报错只能指向整行;拆成三行,每一行都能独立报错:

// 报错指向这一整行,你只能逐段猜
auto result = transform(filter(load(input), pred), mapper);
// 拆开之后,哪一步不成立,错误就落在哪一行
auto loaded = load(input);
auto filtered = filter(loaded, pred);
auto result = transform(filtered, mapper);

对前面那个排序例子,还有一个更直接的修法:告诉编译器按哪个字段排,别让它猜 User 的默认顺序。

std::ranges::sort(users, {}, &User::age);

这行能编译通过,因为它把「比较什么」写进了参数,而不是留给 operator< 去推导。

六、让报错变短,是 API 设计的一部分#

C++20 的 concepts 把约束提到了接口上:

#include <concepts>
template <std::totally_ordered T>
struct SortedList {};
struct User { int age; };
SortedList<User> users;

这次的报错会直接说「约束不满足」,并指出卡在哪条表达式上,而不是先展开几十层实例化:

error: template constraint failure for 'template<class T> requires totally_ordered<T> struct SortedList'
note: constraints not satisfied
note: the required expression '(__t == __u)' is invalid

同样是一个不满足的约束,报错从「整条调用链」缩成了「哪个 concept、哪条表达式」。约束写在接口上,编译器就不必等到实现深处才发现问题。

Rust 的 where 子句和 #[diagnostic::on_unimplemented]、TypeScript 的具名类型别名,做的也是同一件事:把意图提前写清楚,让报错停在离你近的地方。

报错的长度,大致等于编译器需要向你还原的推理链长度。你把意图写进类型、约束和命名,它就少猜一点,也少写几屏。

七、结语#

下次终端刷出四屏红字,先别急着 clear。开头那条 error: 告诉你哪里断了,required from here 告诉你它是怎么走到那里的,note: candidate 告诉你它还考虑过哪些别的可能。把它当成一份附赠的调用栈,比当成噪音划算。

当然,如果你只是漏了一个分号,以上都不适用——报错开头就写着 expected ';',剩下几十行是编译器在陪你一起尴尬。

参考来源#

编译器报错大赏:一句话能写几屏
https://www.hehonglei.cn/posts/compiler-error-pageant-how-to-read-long-errors/
作者
Honglei He
发布于
2026-10-31
许可协议
CC BY-NC-SA 4.0