C++:异质查找(heterogeneous lookup)

太久没更新博文了,水一篇凑数 从 String View 说起 C 风格的字符串常常需要自己记录长度、管理生命周期,涉及长度变化时更是比较麻烦。于是在 C++ 中我们有了 std::string,并且有了与之配套的一系列函数,比如 std::stoi,对应 C 里面的 atoi。这个函数的声明如下: 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& 减少了一次指针跳转。...

2021-10-16 · QuarticCat

C++ 名称查找的又一个恶心设计

众所周知,C++ 的名称查找一直以来都很反直觉。比如臭名昭著的 ADL,这玩意经常在意想不到的地方恶心到你,还往往难以排查。具体表现为你在当前的命名空间里自己定义了一个函数,结果在调用它的时候编译器却找到了十万八千里外的另一个同名函数,即使你没有在当前命名空间引入该函数。下面是两个很常见的例子: namespace fuckadl { struct Fuck {}; void foo(Fuck) { puts("not mine"); } } // namespace fuckadl // case 1 void foo(fuckadl::Fuck) { puts("mine"); } // // case 2 // template<class T> // void foo(T) { puts("mine"); } int main() { foo(fuckadl::Fuck{}); } 对于 case 1 ,你会得到一个编译错误。编译器告诉你不知道该选择哪个 foo 函数。而对于 case 2,你甚至一个警告都不会得到,编译器自动就选择了 fuckadl::foo 。假如是不知道这个 feature 的 C++ 新人,怕是 debug 一天也找不到哪里出了问题。 这种设计直接破坏了命名空间的封装意义,要知道无数的 header only 库都在用命名空间来对外隐藏内部符号(谁让 C++ 的模块机制拖延了这么久呢),你都没法知道什么时候就和别人函数名字撞上了。 最近发现了 C++ 名称查找的又一个恶心设计。来请出我们的主角,C++ Standard Draft N3337 10....

2021-06-23 · QuarticCat

C++:把字符串编码进类型里

又到了我第 114514 喜欢的类型体操环节。 这玩意有什么用 非要举一个具有实用意义的场景的话,PEGTL 是我能想到的一个很好的例子。这是一个 parser combinator 库,它使用类型来组合 parser ,比如这样: struct separator: star<one<' ', '\t', '\r', '\n'>> {}; 那么要 parse 一段字符串的时候当然就要把字符串信息编码进类型里面了。 我在自己的玩具 parser combinator 库里也用了这种方法,只不过我写法上使用变量来组合。 除此之外,著名的 fmt 库也用到了这个东西来进行大量的编译期字符串操作。但它为了兼容性,实现方式都比较原始,而且重复实现了大量标准库中后来加入或者被标记为 constexpr 的东西,也许我有时间会用 C++20 实现一个简易版的 fmt 库。 不就是个 char... 吗 看了上面的例子,肯定有人会这么想。但其实再想想,我们可以有好几种方案在模板参数里接收一个字符串: template<const char* /* , size_t N */> struct Str1 {}; template<char...> struct Str2 {}; // Need C++20, will explain later template<SomeUserDefinedString> struct Str3 {}; 为什么这里不写数组类型呢,因为在模板参数里,数组类型会被自动替换成指针,和第一种方案实际上是一样的。总之就这么三个,我们一个一个来讲。 方案一 template<const char* /* , size_t N */> struct Str1 {}; 把它放在第一个讲是因为它是最废物的,只要一写就发现:...

2021-03-05 · QuarticCat

C++:编译期类型信息

引言 尽管标准库中已经有了 typeid 运算符,但是由于其需要支持检查多态类型,带来了非常多的限制: 它必须启用 RTTI(Run-Time Type Information)。而很多项目是禁用 RTTI 的,所以无法使用 typeid。 它可能对表达式进行求值,详见 cppreference 。这可能带来意外的运行时开销甚至副作用,尤其是常用的 sizeof 和 decltype 都是完全静态的,不熟悉 typeid 的程序员可能完全意识不到这种动态行为的产生。 它不是 constexpr 的,即使其类型本可以静态求出。这意味着很多场景都无法使用 typeid,比如模板参数、switch-case 语句的 case 值等。 这就是我们为什么需要编译期类型信息,即 CTTI(Compile-Time Type Information)。 实现原理 C++ 标准中并没有提供相关的设施供我们实现这一功能。但通过 GCC 和 Clang 的一个特殊的预定义变量 __PRETTY_FUNCTION__,CTTI 得以实现。这个变量的值是当前函数完整签名的字符串。当在一个模板函数内部调用的时候,也会包含模板参数的类型名,这就达到了我们获取类型名字的目的。 注意,类似于标准中提供的 __func__,__PRETTY_FUNCTION__ 是一个变量,因此它没法被用来初始化字符数组或者跟字符串字面量拼接在一起。MSVC 没有 __PRETTY_FUNCTION__,但是有一个类似功能的宏 __FUNCSIG__,它被替换为一个字符串字面量。 template<typename T> constexpr std::string_view pretty_function() { return __PRETTY_FUNCTION__; } 在 Clang 上,调用 pretty_function<int>() 会返回 std::string_view pretty_function() [T = int],可以看到 T 的类型正在其中。 请注意,这里的 typename T 不能直接省略成 typename,否则将不会出现 T 的类型名。...

2021-02-14 · QuarticCat

C++ 类型阅读入门

C++ 的类型可读性很差,并且大多数入门材料中并没有详细介绍如何阅读它们,最多只是讲到 top-level const 和 low-level const 的区别。有不少朋友问过我这方面的问题,讲得多了,干脆整理起来写篇文章。 常见误解 在详细讲类型的阅读之前,需要纠正一个常见的误解。 问:int a[5] 里的 a 是什么类型的? 答:int[5] 类型,在适当的时候会「退化」(decay)成为 int* 类型。 问:int a[5][6] 里的 a 是什么类型的? 答:int[5][6] 类型,在适当的时候会「退化」成为 int(*)[6] 类型,即指向 int[6] 的指针。 由于「退化」这种隐式转换的存在,很多初学 C++ 的人会把数组类型等同于指针类型。 类似的,函数类型也会「退化」到指针类型,如 int(int, int) 会「退化」成为 int(*)(int, int)。 解方程 当你查阅如何阅读一些复杂的类型时,你可能会看到网络上一些人说 C++ 的类型就是解方程,让我来详细解释一下这句话。 抛去 CVR (const, volatile, reference) 等不谈,C++ 最基本的声明分为两个部分:写在最左边的类型名是「类型说明符」(type specifier),剩下的部分是「声明符」(declarator)。这部分内容是从 C 继承过来的,它们是 C++ 类型里最恶心的地方。但这两个名字实在没啥识别度,我喜欢不严谨地称呼为返回值类型和调用表达式,这点后面会解释。先来看几个例子吧: 声明 类型说明符 声明符 int a int a int* a int *a int a[5] int a[5] int* a[5] int *a[5] int (*a)[5] int (*a)[5] 作出这种分别后,就可以理解上表中a的类型是怎样决定的了:以「声明符」的形式调用a后,得到的返回值类型为「类型说明符」。所谓的解方程就是这样一个过程:...

2021-02-13 · QuarticCat