当年写模板。
最难的不是写。
是解释。
你明明只是想说。
“你得有这个成员函数。”
结果你写出来的代码。
像在玩魔术。
当年:我们把约束藏进实现里
你可能见过这种写法。
#include <type_traits>
template <class T>
auto size_of(const T& t) -> decltype(t.size()) {
return t.size();
}
它看起来像“自动推导返回类型”。
但真正的目的。
是让不满足条件的类型。
别参与重载。
这就是那套老规矩。
SFINAE。
Substitution Failure Is Not An Error。
名字很酷。
读起来很累。
线上啪一下:我只是想写个通用日志
你写了个小项目。
把任何东西都打到日志里。
你想要的是。
能 size() 的就打印长度。
不能的就别走这条路。
你先写了一个模板。
#include <iostream>
template <class T>
void log_size(const T& t) {
std::cout << t.size() << "\n";
}
int main() {
int x = 7;
log_size(x);
}
你传了个 int。
编译器会报错。
但错误的位置。
可能不在 main。
它会从 operator<<。
一路扯到某个头文件。
你会开始怀疑。
“我到底做错了什么。”
C++20:requires 子句
requires 子句。
就是把“我需要什么”。
写在函数签名上。
像门口贴告示。
template <class T>
requires requires (const T& t) { t.size(); }
void log_size(const T& t) {
std::cout << t.size() << "\n";
}
这段代码里。
你会看到两个 requires。
别慌。
它们不是一回事。
requires 子句 vs requires expression
requires 子句(clause)
它管的是。
“这个函数/模板能不能参与匹配”。
你可以把它当成。
函数签名的一部分。
requires expression
它管的是。
“我要求你能写出这些表达式”。
表达式写在里面。
编译器会去试。
试不出来。
就算不满足。
写得更像人话:用 concept 起个名字
你很快会发现。
直接在 requires 里写表达式。
会变长。
那就给它起个名字。
#include <concepts>
template <class T>
concept HasSize = requires (const T& t) {
t.size();
};
template <HasSize T>
void log_size(const T& t) {
std::cout << t.size() << "\n";
}
这时候报错。
就会指向 HasSize。
你看到名字。
就知道条件是什么。
trailing requires:你可以把约束放到后面
有时候你想让模板参数列表保持干净。
你可以把约束放在后面。
template <class T>
void log_size(const T& t)
requires HasSize<T>
{
std::cout << t.size() << "\n";
}
它和前面的写法等价。
只是排版不同。
你可以按团队习惯选。
你刚学 C++ 可能会卡:花括号里还能写返回类型
requires expression 里。
你还能写得更精确。
比如你要求 size() 的返回值能转成 std::size_t。
#include <concepts>
#include <cstddef>
template <class T>
concept HasSize = requires (const T& t) {
{ t.size() } -> std::convertible_to<std::size_t>;
};
{ expr } -> concept 这句。
意思是。
“这个表达式的结果,要满足这个约束”。
这不是箭头函数。
也不是 lambda。
它只是语法。
关键结论
requires 不是为了炫技。
它是为了把模板的“前置条件”。
写在最显眼的位置。
小结
你要的不是“模板更强”。
你要的是。
模板先把要求说清楚。
不满足就别硬匹配。
这才像接口。