你第一次被模板报错教育。
大概率是在半夜。
你改了一个小地方。
然后编译器给你甩了一堵墙。
你开始滚动。
滚动到手麻。
最后发现。
真正的问题只有两个字。
“不行”。
当年:错误信息的锅,不全是编译器的
编译器当然不完美。
但我们也不无辜。
我们写的模板。
经常把“要求”藏起来。
藏在某个成员函数调用里。
藏在某个 decltype 里。
于是错误只会在最深处爆。
线上啪一下:我写了个 append,结果有人传了 array
你写了个小工具。
把元素追加到容器里。
你心里想的是 std::vector。
手上写的是“泛型”。
template <class C>
void append_one(C& c, int x) {
c.push_back(x);
}
int main() {
int a[3] = {1, 2, 3};
append_one(a, 9);
}
你当然知道这不对。
数组没有 push_back。
但错误会怎么报。
有时候它会把你带进。
“表达式无效”。
“候选函数不匹配”。
你刚学完 C。
看到这些词。
只会更慌。
第一步:把要求起名
别急着写复杂。
先把“我需要 push_back”。
写成一个 concept。
#include <concepts>
template <class C>
concept PushBackableInt = requires(C c, int x) {
c.push_back(x);
};
这行代码。
就是在说。
“你能不能写出 c.push_back(x) 这句话”。
写不出来。
就不满足。
第二步:把 concept 放到签名上
template <PushBackableInt C>
void append_one(C& c, int x) {
c.push_back(x);
}
现在你传数组。
会更早失败。
而且错误会指向。
PushBackableInt。
你一看名字。
就知道缺什么。
第三步:把“人话”做得更彻底
Concepts 本身不会让编译器自动生成漂亮文案。
但你可以用一个“兜底重载”。
把错误写成你希望别人看到的那一句。
template <class C>
void append_one(C&, int) {
static_assert(PushBackableInt<C>,
"append_one needs a container with push_back(int)");
}
这样一来。
真正的错误信息里。
会出现那句英文。
你也可以写中文。
只要你们工具链显示得出来。
你会慢慢喜欢的一件事:拆小 concept
不要写一个巨大的 concept。
一口气塞十个要求。
那样失败了。
你还是不知道是哪条没满足。
更好的方式是。
把要求拆成小块。
template <class C>
concept HasPushBack = requires(C c, int x) { c.push_back(x); };
template <class C>
concept HasSize = requires(const C& c) { c.size(); };
template <class C>
concept GrowableIntContainer = HasPushBack<C> && HasSize<C>;
你看。
这更像接口设计。
而不是一次性押注。
关键结论
Concepts 的价值。
不是“模板更花”。
是。
你能把要求写成名字。
把名字写在签名上。
让错误出现得更早。
也更可读。
小结
模板报错这件事。
以前像天灾。
现在你至少能把它改成。
“清晰的规则被违反了”。
读到这里。
你会发现。
编译器其实也没那么可怕。
可怕的是。
我们不把话说清楚。