本篇介绍几个细琐的小特性,可以使代码更加安全可靠。

最常见的情况是采取 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 << ' ';
}

这是可读性最强的方式。

当然,还有许多其他方法,比如迭代器、算法和一些技巧,但在范式上来说,那些方法很难比这里展示的方式更加简洁,就使用来说,记住这里提到的便已足够。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.