你写模板。
最怕的不是写不出来。
是写出来后。
它选错了那一个。
然后你还不知道它为什么选错。
当年:SFINAE 能用,但很像暗门
我们以前做重载分流。
经常靠 enable_if。
#include <type_traits>
template <class T>
std::enable_if_t<std::is_integral_v<T>>
print(T x) {
// 整数版本
}
template <class T>
std::enable_if_t<!std::is_integral_v<T>>
print(const T&) {
// 其他版本
}
这类代码。
写多了。
你会开始担心两件事。
第一。
别人看不懂。
第二。
你自己过三个月也看不懂。
线上啪一下:序列化的时候“走错了路”
假设你写一个小项目。
要把数据序列化成字符串。
你希望。
整数走整数的路。
容器走容器的路。
你先写了一个很天真的版本。
#include <string>
#include <vector>
template <class T>
std::string dump(const T&) {
return "<object>";
}
template <class T>
std::string dump(const std::vector<T>& v) {
return "<vector>";
}
这段看起来没问题。
但现实里。
你不会只遇到 std::vector。
你会遇到 std::string。
你会遇到 std::array。
你会遇到你们自定义的容器。
于是你开始用“范围”来判断。
C++20:用约束写出“意图分流”
先给条件起名字。
#include <concepts>
#include <ranges>
template <class T>
concept Integral = std::integral<T>;
template <class T>
concept Range = std::ranges::range<T>;
然后你写两个重载。
#include <string>
std::string dump(Integral auto x) {
return "<int>";
}
std::string dump(Range auto r) {
return "<range>";
}
这时候有趣的点来了。
如果你传一个 std::string。
它既是 range。
也不是 integral。
它会走 <range>。
直觉一致。
更关键的点:更“具体”的约束会赢
你再加一个更具体的约束。
比如“字节序列”。
#include <ranges>
#include <cstdint>
template <class R>
concept ByteRange =
std::ranges::range<R> &&
std::same_as<std::ranges::range_value_t<R>, std::uint8_t>;
std::string dump(ByteRange auto) {
return "<bytes>";
}
现在。
当一个类型同时满足 Range 和 ByteRange。
会选哪个。
答案是。
选 ByteRange。
因为它更具体。
标准里叫 subsumption。
你可以把它理解成。
“更严格的门禁,优先通行”。
偏特化也一样:约束可以参与选择
你可能还会写类模板。
然后对不同类型给不同实现。
以前你靠偏特化。
现在你还能加约束。
template <class T>
struct Serializer {
static std::string run(const T&) { return "<object>"; }
};
template <class T>
requires std::integral<T>
struct Serializer<T> {
static std::string run(T) { return "<int>"; }
};
这类写法。
读起来很像。
“接口 + 特例”。
而不是。
“某个神秘条件触发了一条隐藏路径”。
关键结论
Concepts 真正改变的是。
你不需要再用 SFINAE 去“制造不匹配”。
你可以直接写。
“什么情况下才匹配”。
小结
重载选择这件事。
以前像黑箱。
现在你能把规则写出来。
而且写在签名上。
让读代码的人。
少猜一点。