使用 C++20 安全地比较不同类型的整型值
本篇介绍几个细琐的小特性,可以使代码更加安全可靠。
最常见的情况是采取 for loop 遍历某个容器,比如:
std::vector<int> v(10);
std::ranges::iota(v, 0);
for (int i = v.size() - 1; i >= 0; --i) {
std::cout << v[i] << ' ';
}
乍看之下,似乎并无问题,但实际上却存在安全隐患,若是 v.size()
的结果大于 std::numeric_limits<int>::max()
,将产生 UB。
倘若你使用了类型推导,问题会更加明显。
for (auto i = v.size() - 1; i >= 0; --i) {
std::cout << v[i] << ' ';
}
这会输出超出预期的结果!i
被推导为 unsigned
整型,i >= 0
将永远为真。
这种隐患来自于类型的隐式转换,一般编译器只会给出警告。最简单的解决之法就是保证整型符号的一致性,例如:
for (size_t i = v.size() - 1; i < v.size(); --i) {
std::cout << v[i] << ' ';
}
结束条件也随之变为检测数据范围,以避免条件在逻辑上的无效性。但如此一来,可读性直线降低,C++20 引入了几个与此相关的小特性,可以更安全地解决该问题。
第一个是一系列整型比较函数,它们可以安全地对不同符号的类型进行比较。如:
-1 > 0u; // true
std::cmp_greater(-1, 0u); // false
因此,可以用来安全地比较不同符号的整型。
for (int i = 0; std::cmp_less(i, v.size()); ++i) {
std::cout << i << " " << v[i] << '\n';
}
通过使用这些安全的比较函数,代码隐患随之消除。只是无法逆序遍历了,逆序时将 size_t
赋值到 int
依旧有可能产生 UB。
此种情境,更好的方式是采用 std::ssize()
,它是一个有符号的 size()
辅助函数,表意更加直接。代码更改为:
for (int i = ssize(v) - 1; i >= 0; --i) {
std::cout << v[i] << ' ';
}
得益于 ADL,std::ssize()
可以简写为 ssize()
。
当然,以上只是示例需要,对于数据遍历,Range-based for loop 是更好的方式,这样能够避免很多易被忽视的错误。
for (const auto& elem : v) {
std::cout << elem << ' ';
}
通过 C++20 Views,还可以在遍历时组合其他操作,如:
for (const auto& elem : v | std::views::reverse) {
std::cout << elem << ' ';
}
这是可读性最强的方式。
当然,还有许多其他方法,比如迭代器、算法和一些技巧,但在范式上来说,那些方法很难比这里展示的方式更加简洁,就使用来说,记住这里提到的便已足够。