太久没更新博文了,水一篇凑数

从 String View 说起

C 风格的字符串常常需要自己记录长度、管理生命周期,涉及长度变化时更是比较麻烦。于是在 C++ 中我们有了std::string,并且有了与之配套的一系列函数,比如std::stoi,对应 C 里面的atoi。这个函数的声明如下:

1
int stoi(const std::string& str, std::size_t* pos = 0, int base = 10);

这个接口接受一个const std::string&,乍看或许是理所应当的:我是 C++ 函数,我需要读取字符串,但是我不需要修改它。实际上,C++ 中有很多接受const std::string&的函数,然而很遗憾,这个设计是失败的。

考虑这样一种情况,我们需要读取一个std::string里的子串,但我们不需要修改它。比如对一个拥有很多数字的字符串进行连续 parse ,或者在某个大文本里找到某个模板再对匹配结果进行进一步筛选。这种情况下,这些接受const std::string&的函数就变得不好用了,因为子串不是一个std::string对象。我们往往不得不把子串复制到一个新的std::string对象里,造成了额外的开销。

类似的常见情况还有,我们接收到了一个 C 风格的字符串,以char* str+size_t len的形式,而我们希望能在这个字符串上使用各种 C++ 函数的功能。比如跟 C 接口交互的时候,或者用 buffer 从别的地方接收字符串数据的时候。

所以要怎么解决呢?我们可以采用 C 风格的接口,即char* str+size_t len,或者采用迭代器风格的接口,即char* begin+char* end。而将这两个参数合起来,我们就得到了std::string_view——只读字符串接口的正确答案。它不仅比const std::string&更泛用,而且在没有发生 SSO 的情况下,它还比const std::string&减少了一次指针跳转。

Rust 很早就想明白了这个问题,一开始就提供了String(对应 C++ 的string)和&str(对应 C++ 的string_view),并且在各个接口上统一了用法。而 C++ 则是群魔乱舞,什么样的接口都有。

泛型的困境

假设你现在在写一个泛型容器map<K, V>,你要给他添加一个.find成员来进行查找。那么.find的参数应该是什么呢?一般来说const K&就可以了,但要是K = std::string,那么就会遇到前面所说的问题了。给std::string做一个特化吗?不,我们要考虑更一般的问题,即如何在泛型里处理一个类型有多种表示的情况。我们应该允许.find的参数是其他一些和K有关系的类型。C++ 的异质查找( heterogeneous lookup )说的就是这么样一种操作,异质在这里指的是存储的类型和查询的类型不一样。

要解决这个问题也并不是很困难。在std::map等容器中原本就有一个Compare泛型参数用于设置比较器,我们只要接收一个可以比较不同类型的比较器并且把.find的参数放宽到任意类型就可以了。

默认情况下,Compare的值是std::less<Key>,其operator()函数只能用于KeyKey的比较。在 C++14 后,std::less等函数新增加了void特化,其operator()函数是一个模板,接受任意类型并返回std::forward<T>(lhs) < std::forward<U>(rhs)。我们只要将std::mapCompare参数设置成std::less<>即可使用异质查找。

1
2
3
using namespace std::literals::string_view_literals;
std::map<std::string, int, std::less<>> foo;
foo.find("key"sv);

或者你也可以定制自己的比较器,只要它有个合法的成员类型is_transparent,也会启用异质查找。

以上是对std::mapstd::set而言的。对于 unordered 系列容器,则是要求Hash::is_transparentKeyEqual::is_transparent。另外 unordered 系列容器的异质查找在 C++20 才被加入。

Rust 里的解决方案则是用一个Borrow trait 来描述这种关系,相对来说约束更大一些。有兴趣的可以自行阅读文档,这里就不展开讲了。

很遗憾 C++ 太晚意识到这些问题了,导致留下了大量的失败设计。为了兼容性考虑,异质查找不能默认启用,这导致启用了异质查找的和不启用的两个std::map类型是不一样的。所以即使这项功能已经可用,你也可能受限于旧有接口。