zclll's blog - C++
type: channel
link: https://zclll.com/index.php/category/cpp
items: Linux更新时区文件，解决夏令时和Africa/Casablanca问题
items: 2023开发者生态，一些有趣的事实（偏C++向）
items: How to get started in C++
items: std::make_pair传递引用的问题
items: [C++ quiz] day6
items: 占位类型说明符：decltype(auto)
items: C++性能表现上的技术报告（TR18015） - Exception Handling 异常处理（中文翻译）
items: C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用
items: [C++20]ranges、views与管道（pipeline）语义初窥
items: committee的取舍——为什么显式实例化可以打破访问控制

Linux更新时区文件，解决夏令时和Africa/Casablanca问题
type: item
date: 2024-05-25T17:29:45+00:00
description: 这篇文章用来解决两个问题：如何更新操作系统的时区文件，让它得知目前最新的夏令时信息（这会依当局的行政命令而发生改变）如果遇到了时区信息错误，如何解决第一个需求最为常见，然而第二个问题对于 CentOS 用户来说同样有一定意义。下次有其他倒霉蛋遇到这个问题的时候，就能从本文确定问题和修复方式了。问题当你使用的应用不自己维护时区信息时，他们会从 Linux 系统的 tzdata，也就是系统的时区数据库读取这些信息。这一般依赖$TZDIR指示的路径（因此你可以通过这个环境变量修改它——如果你的应用程序足够聪明，知道先看看这个变量的话），如果没有指示，那么默认是/usr/share/zoneinfo目录。如果你的场景强依赖正确的夏令时数据，那么你必须经常更新这个数据库。因为很多主体的夏令时每年并不一致，而这依赖于当局的行政命令，因此你需要根据新的公告数据去更新，以保证你的软件和现实世界表现一致。如果你使用 CentOS，在某些涉及到时区的场景中，发现涉及 Africa/Casablanca（卡萨布兰卡）这个时区的数据总是不对，而其他绝大多数情况都是正常的，那么大概率是因为：这儿的 tzdata 本来就是坏的。如何解决你需要做的是更新系统的时区文件到一个正确/最新的版本（取决于你的需求）。接下来是过程：首先你需要下载 IANA 时区数据库，这里我们就用最新的，其他也很容易在 ICANN 找到：wget https://www.iana.org/time-zones/repository/tzdb-latest.tar.lz将其解压，然后完成 build 过程，这可能需要 C 编译环境：tar -xf tzdb-latest.tar.lz cd tzdb-2024a/ make TOPDIR="$HOME/tzdir" install # 决定编译到哪里 "$HOME/tzdir/usr/bin/zdump" -v America/Los_Angeles # 读一个文件验证一下然后你在TOPDIR给定的路径就得到了一份新的 tzdata 及相关文件。接下来需要用它们去覆盖本系统中的文件（如果需要，记得备份）：sudo cp -r $YOUR_PATH/usr/share/zoneinfo /usr/share这就足够更新时区数据了。当然如果你需要更全面的更新，包括一些时区相关工具，那么你可以把TOPDIR所有的文件拷贝并覆盖到/目录对应位置下。参考资料Africa/Casablanca timezone missing #10211Africa/Casablanca timezone issue #1442作者在 SelectDB 的内部文档

2023开发者生态，一些有趣的事实（偏C++向）
type: item
date: 2024-01-08T04:13:44+00:00
description: [scode type="blue" size=""]开拓视听，颇具参考意义。[/scode]语言——JS，yyds！（Rust长势喜人——越难找的员工，越值钱啊——Rust，唯一原神——这么多的人在用C++做游戏？这么少的人用C++写数据库吗。。职业生涯——学历？——内生的兴趣，是计算机行业的独到优势啊——好吧，这个比例远超我的想象AI——毕竟，这是一个需要知识和智力的行业——确实，谁又爱写测试呢数据库——interesting~ redis已经作为八股融入了cn后端人的血液了。另外，在这里好像大家不那么愿意接受新鲜事物（指pgsql——Navicat，CN特供！（破解版）开发者——嗯。。入职即高级！——码农是青春饭（——看起来很有信服力——呃。。。——青春饭和老龄化社会——实际润人的数量开发工作——woo，相当高的比例C++——比我们想象的要快，不是吗？——哈哈，大家都很想要modules，但是谁又能完整支持呢？——好吧，这么多人不写单测——我们需要（而且没有）一个合理的解决方式——GCC远胜于CLANGRust——新兴力量，不是吗？参考文献以上全部数据，来自JetBrains每年进行的开发者生态调查，原文刊于https://www.jetbrains.com/zh-cn/lp/devecosystem-2023/

How to get started in C++
type: item
date: 2023-04-16T15:27:00+00:00
description: 在大家的不懈催更下（hhh)，这篇文章终于和大家见面了。对于程序设计、软件开发而言，我都只是一个入门水平而已（也许C++略多一点，但也并不很多）。对任何知识的掌握，都不足以保证完美无缺。那么有什么可以和大家说的呢？我想，只有对一些事物的理解与认知。如何快速地养成一位普通的C++工程师，是培训班应该去做的任务。而我们，应该为成为一名优秀的软件工程师打好基础。这些内容显然不会是无上真理，因为它受限于笔者的有限认知。但至少，做到这些内容，成为比笔者更优秀的C++工程师，还是非常容易的。最重要的对于学编程的新手来说，最重要的事情是——先把代码写起来。这件事情对于各个行业来说都很重要，但也许对coding尤为重要。如果说程序员这个行业一定需要一些天分的话，那么最重要的一个，就是有“先把事情搞起来”的执行力。这意味着，也许你还在犹豫“我应该学什么，用哪本书、学谁的课”，什么时候学，要不要沐浴更衣，要不要择良辰吉日——都不必。就用你手头有的资源，写你想到的东西，先写起来。如果没有合适的，那么在这篇文章中，我会提供一些——但它不会是本篇文章的重点。C++需要学什么被问最多的问题是：“有没有推荐的学习路线？”要解答这个问题，必须要解决几个前置问题：你得明确，学C++是为了什么？为了赶紧找一份不错的工作？为了做有价值的东西？为了做很酷的东西让自己开心？自己要去做哪个方面的开发？是hpc，服务端，嵌入式，量化……还是什么？让我们一个一个来说。第一点，你的动机决定了你要去做什么。因为人的精力是有限的，你不可能对C++这么辽阔的版图面面俱到。特别是，C++的世界中有一类特别的人——语言律师，他们对复杂的语法规则——你绝不可能了解的那些——如数家珍，但不一定是非常优秀的软件工程师。你需要决定自己要不要成为这样的人。这并没有优劣之分，只是不同的选择。就像“茴字有几种写法”一样。当然需要有人知道（比如计算机史学家），但如果你希望成为一名优秀的coder的话，显然不是你需要知道的事情。那么，掌握一门语言需要到什么程度呢？对语言的掌握C++是一门非常复杂的语言，与其他语言最大的不同在于，它有一套被称之为“标准”的东西1 。这套标准非常详尽，事无巨细地规定了C++世界中的每一个角落该长什么样子。也正因为如此，C++世界中有一个经典的笑话，关于 “面试者在简历上写了精通C++结果面试官出了一堆元编程打击得面试者无地自容的那些事儿”。这意味着我们永远都不能够说自己“精通C++”了么？我看并不是。很多群友都知道，我在自己的简历上的确写了“精通C++”。因为在我看来，自己已经完全满足这个要求了。我能保证做出cpp quiz中的hard题吗？我不能。给我几个超复杂的候选函数，我能搞懂重载决议会pick哪个吗？我不能。但让这些炫技的东西出现在一个正经项目的代码里，比“不会C++”可怕一万倍。我能保证的是，对于我们的项目中所涉及到以及可能涉及到的C++知识，我已经掌握了至少99%了。我的精力应该放到如何写出更简洁、低耦合、可复用的代码上去，应该研究如何让我的代码对CPU、缓存更加友好，应该研究各个组件的架构应该如何设计才能从宏观角度提高性能和稳定性……——而不是让那些超复杂且有害的东西来占据我本就有限的时间2 。如果一个机制需要非常复杂的语法去实现它，那么相信我，一定有更好的办法。如果你在cr的时候看到了一个完全看不懂的写法，那么相信我，give it a -1.这是第一点：你必须清楚自己学C++是为了什么。 花一些时间做cool的事情来取悦自己没什么不好，但长期来看，你得大体坚持在正确的道路上，才能写出真正很酷的东西。它会是一个从性能、软件工程等角度上都很酷的东西，而并不因为你采取了很先进或很复杂的语法。先进的语法只有在让代码更简单的时候才是真的先进；复杂的代码只有让更多的代码更简单/性能更高的时候才是合适的复杂。所以——比语言更重要的专业能力语言永远只是一个工具。我想大多数人学C++的目的，是因为它容易写出可掌控、高性能的代码，这就意味着语言不是第一性的，它是为了功能而服务。如果对业务确实有利，那么明天去写Rust又怎么样呢？任何人都不应该抵触在合适的时机选择更合适的语言。C++也许的确更难学，但不要因为学了它而拥有可悲的壁垒。你的领域知识远比语言更重要。所以，你应该去考虑：我要成为一名hpc开发？还是嵌入式开发？还是后端开发？……只不过你喜欢C++，那么你可以选一些用C++比较频繁的领域，把C++作为你的核心竞争力之一，而不是“一棵树上吊死”。当然，语言是这一切的基础。所以你首先得把C++[font color="BlueViolet"]学到一个“能写出各类功能代码”的程度，然后就要尽快让领域知识驱动着你去学习和coding[/font]。而这并不难。我们预期花多少时间呢…

std::make_pair传递引用的问题
type: item
date: 2023-03-23T14:36:00+00:00
description: C++中一个隐蔽的小坑：std::pair<const ColumnPtr&, bool> unpack_if_const(const ColumnPtr& ptr) noexcept { // ... return std::make_pair(ptr, false); }以上代码有什么问题？如果没有看出来，我们可以搞一个最小可复现实例：std::pair<const int&, int> f(const int& arg) { return std::make_pair(arg, 1); } int main() { int a = 1; const auto& [b, _] = f(a); a = 2; std::cout << b; return 0; }运行一下，会发生什么？我们会发现输出结果是：1说明我们没能够正确地通过std::pair传递arg的引用。其实这时候b捕获的是一个临时对象的引用。这是因为：std::make_pair默认不按引用捕获，而是通过std::decay捕获到非引用类型，从而按值传递！正确写法同时，上面的信息也给出了对应的解决方案：用引用包装器传递参数。因此我们可以将最初的代码改写成：std::pair<const ColumnPtr&, bool> unpack_if_const(const ColumnPtr& ptr) noexcept { // ... return std::make_pair(std::cref(ptr), false); }就能使得std::make_pair正确捕获引用了。当然，pair的默认构造函数不会进行相似的decay，因此我们可以通过初始化列表去完成一样的目的：std::pair<const ColumnPtr&, bool> unpack_if_const(const ColumnPtr& ptr) noexcept { // ... return {ptr, false}; }

[C++ quiz] day6
type: item
date: 2023-02-26T15:19:00+00:00
description: 1 整型提升#include <iostream> void f(unsigned int) { std::cout << "u"; } void f(int) { std::cout << "i"; } void f(char) { std::cout << "c"; } int main() { char x = 1; char y = 2; f(x + y); }答案：Implement Defined。解析首先：“signed char”, “short int”, “int”, “long int”, and “long long int”. In this list, each type provides at least as much storage as those preceding it in the list.也就是说int不一定比char更大，只有不小于的关系是得到保证的。我们知道加法会首先对操作数进行整型提升，而又因为：It is implementation-defined whether a char object can hold negative values.所以当我们需要将它提升到可容纳它的（至少是int的）类型时，如果当前环境char可以包含负数，x+y就是signed char+signed char得到int；反之，x+y就是unsigned char+unsigned char得到unsigned int。而产生分歧的原因是ID不是UB，所以最终的结果是ID的。2 重载决议中的隐式转换#include <iostream> void f(float &&) { std::cout << "f"; } void f(int &&) { std::cout << "i"; } template <typename... T> void g(T &&... v) { (f(v), ...); } int main() { g(1.0f, 2); }答案：if解析首先, 在进入g()以后, 我们实际产生的调用是g(float&& v1 = 1.0f, int&& v2 = 2), 然而这两个右值引用的值类别是左值! (关于这一点, 我的文章的#0302节有详细的解释, 不再赘述).确认了这一点, 那么9行实际上就会产生f(float)和f(int)先后两个调用. 那么结果是fi吗? 此时就要进行一下重载决议了, 而这时就有一件容易被遗忘的事情: 右值引用不能绑定到左值上!从而f(1.0f)不能调用f(float)重载, int那一支同理.然而, 经由标准转换序列中的数值转换, int左值可以转换为一个float亡值, float左值也可以转换为一个int亡值, 从而f(1.0f)可以调用f(int&&), 而f(2)可以调用f(float&&)!于是展现给我们的结果就是一个float调用了int重载, int调用了float重载, 似乎"颠倒"了过来.这件事情看起来诡异, 但其实质是合理的: 左值和右值之间不能随意转换, 否则区分左右值就没有意义了. 看起来相比于float&&, int转化成int&&更为自然, 但这种转换是std::move的任务. 而隐式转换序列在重载决议时又是应当允许的, 这二者组合就产生了这一看似奇怪的结果.3 初始化列表#include <iostream> #include <vector> int f() { std::cout << "f"; return 0;} int g() { std::cout << "g"; return 0;} void h(std::vector<int> v) {} int main() { h({f(), g()}); }答案:fg解析函数实参求值是不定序的, 然而这里有一个初始化列表, 初始化列表是by order求值的.当然, 前提也是因为std::vector有接收init_list的构造函数重载. 如果一个类型既没有这样的重载, 又不是聚合类(不能聚合初始化), 那么这里也有隐含的一个坑.4 静态成员初始化#include<iostream> int foo() { return 10; } struct foobar { static int x; static int foo() { return 11; } }; int foobar::x = foo(); int main() { std::cout << foobar::x; }答案:11解析静态数据成员是可以延迟定义的, 此时类内的声明不是默认定义. 从而17行是static变量的有效定义, 而它本质上还在类内, 所以根据static成员初始化的调…

占位类型说明符：decltype(auto)
type: item
date: 2022-12-02T13:05:00+00:00
description: 一则简短的小知识： decltype(auto)是干嘛的？auto作为占位类型说明符自C++11就出现了，它本质上是个语法糖，能够让我们把type var = expr;简写成auto var = expr;，主要带来了两方面的好处，一是减少了代码的复杂程度，特别是type = std::some_container<some_type>::some_iterator这种情况，降低了编码的负担；另一方面提高了代码的统一程度。[scode type="blue" size=""]那么既然auto已经足以推导类型了，我们又为什么需要decltype(auto)的存在？[/scode]看起来auto和decltype都起到了推导的功效，为什么还要多此一举呢？考虑一个最简单的例子：template<typename T> auto f(T&& arg) { return std::forward<T>(arg); }我想要在模板代码中，将参数按照原本的值类别引用转发出来，然而这是做不到的，auto只会得到非引用的类型：#include <bits/stdc++.h> template<typename T> T check(); // 一种奇特的编译期值类别检测技巧 int a = 1; template<typename T> auto f(T&& arg) { return std::forward<T>(arg); } int main() { check<decltype(f(a))>(); // undefined reference to `int c<int>()' check<decltype(f(1))>(); // undefined reference to `int c<int>()' return 0; }原因是auto 自身不包含值类别和cv限定，它的推导规则是依据模板实参推导来的：这样，当我们在写一些模板代码的时候，如果需要返回转发引用，似乎只能抛弃万能引用而分别实现了。或许有人想到了使用auto&&，但当我们需要处理在返回值中转发引用的时候，这种方法也失效了，因为它还可能包含非引用类型：template<typename T> /* ref or non-ref */ g(T); template<typename T> auto&& f(T&& arg) { return std::forward<T>(arg); // 解决了 return g(std::forward<T>(arg)); // 不奏效 }也就是说，我们需要在返回值上引入一个真正的完美转发。为了解决这一问题，C++在14就引入了decltype(auto)这一特定搭配写法，用于推导出带引用类别的值类型。其推导结果将等价于decltype(expr)，其中expr为对应位置的初始化器（在这里是return的操作数）。于是上面的代码就可以实现为：#include <bits/stdc++.h> template<typename T> T check(); // 一种奇特的编译期值类别检测技巧 int a = 1; template<typename T> decltype(auto) f(T&& arg) { return std::forward<T>(arg); } int main() { check<decltype(f(a))>(); // undefined reference to `int& c<int&>()' check<decltype(f(1))>(); // undefined reference to `int&& c<int&&>()' return 0; }[scode type="share" size=""]有一点需要注意的是，对于一个名字进行decltype，为了能够正确区分引用和非引用，嵌套括号的decltype((a))将产生左值引用，而decltype(a)不会。[/scode]这种手法主要用处在于完美转发上：template<class Fun, class... Args> decltype(auto) Example(Fun fun, Args&&... args) { return fun(std::forward<Args>(args)...); }通过decltype(auto)，如果fun返回引用，我们实现了正确地将引用转发出去，非引用的结果也是正确的（返回值的完美转发）。这种情形下，它是唯一的方法。其实，在理解上，这仍然可以看做是一个语法糖，auto就是起到了占位符的功能。例如上面的代码，其实就是通过decltype(fun(std::forward<Args>(args).…

C++性能表现上的技术报告（TR18015） - Exception Handling 异常处理（中文翻译）
type: item
date: 2022-11-17T08:48:00+00:00
description: 5.4 异常处理异常处理提供了一个系统化且健壮的方法来处理那些无法在它们被检测到的地方就地处理完成的错误。传统的异常处理方法（在C、C++和其他语言中）包括：返回错误代码（return非零值）设置错误状态指示器（如errno）调用错误处理函数使用longjmp直接从上下文跳转到错误处理代码每次调用时传递一个指向状态对象的指针在考虑异常处理时，必须将其与其他处理error的可能方式进行对比。比较时，合理的考虑范畴包括：编程风格错误处理代码的稳健性和完整性运行时系统（内存）开销处理一个错误的额外开销考虑一个平凡的例子：double f1(int a) { return 1.0 / a; } double f2(int a) { return 2.0 / a; } double f3(int a) { return 3.0 / a; } double g(int x, int y, int z) { return f1(x) + f2(y) + f3(z); }这段代码不包含错误处理程序。在C++异常处理机制出现之前，有几种技术可以检测和报告错误：void error(const char *e) { // handle error } double f1(int a) { if (a <= 0) { error("bad input value for f1()"); return 0; } else return 1.0 / a; } int error_state = 0; double f2(int a) { if (a <= 0) { error_state = 7; return 0; } else return 2.0 / a; } double f3(int a, int *err) { if (a <= 0) { *err = 7; return 0; } else return 3.0 / a; } int g(int x, int y, int z) { double xx = f1(x); double yy = f2(y); if (error_state) { // handle error } int state = 0; double zz = f3(z, &state); if (state) { // handle error } return xx + yy + zz; }理想情况下，真正的程序会使用一致的错误处理风格，但这种一致性在大型程序中往往很难实现。请注意，error_state技术不是线程安全的，除非实现提供对线程unique static数据的支持，而且用if (error_state)产生的分支可能会干扰处理器中的管道优化。还要注意的是，在error()可能不会终止程序，这种情况下程序很难高效率地使用它。然而，这里的关键点是，任何处理不能在本地处理的错误的方法都意味着空间和时间的额外开销。它还会使程序的结构复杂化。使用“异常”，这个例子可以写成这样：struct Error { int error_number; Error(int n) : error_number(n) {} }; double f1(int a) { if (a <= 0) throw Error(1); return 1.0 / a; } double f2(int a) { if (a <= 0) throw Error(2); return 2.0 / a; } double f3(int a) { if (a <= 0) throw Error(3); return 3.0 / a; } int g(int x, int y, int z) { try { return f1(x) + f2(y) + f3(z); } catch (Error &err) { // handle error } }在考虑异常处理的开销时，我们必须要通盘考虑可选的异常处理技术的成本。Exception的使用将错误处理代码与正常的程序执行流隔离开来，而且与error code方案不同，它无法被忽略。另外，在抛出异常时自动销毁栈上对象，使程序更不容易泄漏内存或其他资源。使用exception，一旦发现问题，它就无法被忽略——如果不能捕捉和处理异常，就会导致程序终止。关于使用异常的技术的讨论，见 "The C++ Programming Language"的附录E [BIBREF-30]。异常处理的早期实现导致了代码规模的显著增加和一定的运行时overhead。这导致一些程序员会选择避免使用它，编译器供应商也提供开关来抑制异常的使用。在一些嵌入式和资源受限的环境中，由于担心overhead，或者由于异常机制的实现不能满足项目…

C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用
type: item
date: 2022-11-15T20:35:00+00:00
description: [scode type="green" size=""]照例先来一个30s速览版本：[/scode]CP（定制点）指的是对于一个特定名称、特定功能的函数调用可能需要派发给不同具体实现的调用点，此谓之“定制”；CPO（定制点对象）是用来解决定制点问题的一个工具；CPO的实现原理是：写成函数对象+operator()的实现，根据规则，二阶段的ADL查找在一阶段名字查找发现对象实体的时候不发生；niebloid是指ranges算法的调用。它和CPO的关系是：niebloids现阶段使用CPO作为具体实现方式。那么，正文开始：CPO和niebloid应该说是C++20当中最为晦涩的概念了，对于大多数人来说，我们永远都不需要自己去实现这种东西。但它们当中蕴含的技巧非常之巧妙，能够让我们对函数调用和库功能设计的理解提高一个档次。1 Intro大多数讲解CPO的文章都会提到swap的例子，我们这里也不能免俗。简单来说，我们考虑如何交换两个对象：最基本的做法是，我们可以swap(a, b)，标准库有一个经典的三赋值实现，这很容易理解。然而问题来了，[font color="Crimson"]如果这个实现不是我们想要的呢？[/font]Situation——定制点一个典型的情况是，我们要交换的对象是pimpl的：[collapse status="false" title="pimpl对象"]PIMPL（pointer to implementation，指向实现的指针）是一种C++当中的编程技巧，在类的公开接口中不包含具体实现，而是使用指针指向一个实现类，而实现类可以在分离的翻译单元中进行开发。它的原理是，指针可以指向不完整类型（因为指针指的是被指对象首地址，而不需要知道对象具体内存情况），这样我们在定义中使用一个前置声明，就可以把实现扔出当前翻译单元了：// in header file class widget { public: widget(); ~widget(); private: class impl; impl* pimpl; }; // in implementation file class widget::impl { // ::: };这样就形成了所谓的“编译防火墙”：对实现类impl的改动不会引发使用了对应header的文件的重编译，极大地减轻了编译期的依赖；对类成员布局的修改，也不会破坏接口的ABI。[/collapse]对于这样一个对象，我们如果调用三赋值的swap会发生什么呢？那会引发三次深拷贝，因为pimpl类型通过指针管理实际对象，显然这里调用的拷贝构造（赋值），我们会把它实现成深拷贝的。而这里我们实际需要做的是交换pimpl指针就可以了。这种情况下，我们应该自己去实现一个特殊的、高效的swap(widget&, widget&)函数，然后调用这个实现。也就是说，出现了这样一种情况：[font color="red"]我们希望对同一个函数名的调用，会根据不同的条件（主要是特定类别参数）实际调用不同的合适实现。[/font]这样的函数调用点，我们就称之为[font color="red"]定制点[/font]。它定制的是处理不同的实参时，函数不同的实际表现。怎么做呢？容易想到两种思路：Method 1——模板特化 ×最容易想到的就是给对应函数直接添加特化，然而这并不总是有效。原因是函数模板无法偏特化，见下例：namespace My{ struct A{}; template<typename T> struct B{}; } template<> void std::swap<::My::A>(::My::A& a, ::My::A& b) noexcept(__and_<is_nothrow_move_constructible<::My::A>, is_nothrow_move_assignable<::My::A>>::value) {} // ok template<typename T> void std::swap<::My::B<T>>(::My::B<T>& a, ::My::B<T>& b) noexcept(__and_<is_nothrow_move_constructible<::My::B<T>>, is_nothrow_move_assignable<::My::B<T>>>::value) {} // ill-form这里对My::A的特化是成功的，然而My::B<T>则不行，因为这属于一种偏特化，而函数模板是无法进行偏特化的。因此像hash<user_type>一样添加模板特化的方式，并不是我们需要的答案。既然函数模板不能偏特化，而一般我们要偏特化函数模板的时候，都是通过fu…

[C++20]ranges、views与管道（pipeline）语义初窥
type: item
date: 2022-10-31T18:21:00+00:00
description: [scode type="blue" size=""]这篇文章是一个使用、编写向的源码速读笔记，不进行深度分析。所分析代码基于C++20标准定义接口和libstdc++，gccversion为11.2.0[/scode]Range——作为C++20四大组件之一——给我们提供了一套全新的便捷而友好的手法去实现管道语义（pipeline）。现在我们可以像很多现代语言一样，去对一个范围进行连贯的复杂操作，而不需要函数调用的嵌套了！比如说，我们可以简单地用这样一段代码去实现输出100以内的倒数10个素数：int main() { for (int x : views::iota(1, 100) | views::filter(isprime) | views::reverse | views::take(10) | views::reverse ) std::cout<<x<<' '; return 0; }输出是53 59 61 67 71 73 79 83 89 97最重要的是，它不仅如此的方便，而且是zero-overhead的！那么这是怎么做到的呢，我们来对照源代码逐步分析：分析1 ranges我们都知道range可以很容易的代表一个范围（可迭代）对象，有了它我们可以非常方便的调用ranges算法。例如：既然vector存储了一个范围（我们假设它叫arr），那么我们就可以用std::ranges::sort(arr)去代替std::sort(arr.begin(), arr.end())。这是怎么实现的呢？首先，range如何表示一个范围的呢？我们看一下它的定义： template<typename _Tp> concept range = requires(_Tp& __t) { ranges::begin(__t); ranges::end(__t); };喔！具有begin和end属性的对象就可以被看做是一个range——这也非常合理，我们只要能保证它能够通过begin和end去迭代，那么它就具有了作为“范围”被使用的资格。但begin和end的语义怎么保证的呢？它们也有可能表示某个事态的开始和结束，而非我们想要的迭代器。这点我们的ranges::begin和ranges::end当然做了检查，我们以ranges::begin为例： template<__maybe_borrowed_range _Tp> requires is_array_v<remove_reference_t<_Tp>> || __member_begin<_Tp> || __adl_begin<_Tp> constexpr auto operator()(_Tp&& __t) const noexcept(_S_noexcept<_Tp&>()) { if constexpr (is_array_v<remove_reference_t<_Tp>>) { static_assert(is_lvalue_reference_v<_Tp>); return __t + 0; } else if constexpr (__member_begin<_Tp>) return __t.begin(); else return begin(__t); } };可以看到检查是通过requires来实现的，看看调用者是否可以进行begin调用或者具有begin语义（例如数组类型），并且派发到正确的调用。对于通过成员函数和ADL查找提供的迭代器，这里就做了检查： template<typename _Tp> concept __member_begin = requires(_Tp& __t) { { __decay_copy(__t.begin()) } -> input_or_output_iterator; }; template<typename _Tp> concept __adl_begin = __class_or_enum<remove_reference_t<_Tp>> && requires(_Tp& __t) { { __decay_copy(begin(__t)) } -> input_or_output_iterator; };可以看到这里检测了它们是否真的能够提供用来遍历range的迭代器。考虑一个极端情况，如果我们的range提供了begin和end成员，也满足了提供input_or_output_iterator的requires检测，但实际上该迭代器并不是用来迭代范围的怎么办呢？答案是没有办法。形式检查无法检测实质，确实是concept体系所面临的一个普遍问题，只能通过程序员自己意识到才能保证不发生这种情况。幸运的是这不难避免。当…

committee的取舍——为什么显式实例化可以打破访问控制
type: item
date: 2022-10-12T19:42:00+00:00
description: 在考虑一个经典面试题——“如何在类外访问类的私有成员”的时候，群友在网上发现了如下一个做法：通过显式实例化对access control打洞#include <cassert> #include <iostream> class A { public: int X() { return x_; } private: int x_; }; int A::*FiledPtr(); //返回A的int成员指针的函数声明 template <int A::*M> //这里A是非待决的一个限定，该模板形参是A成员指针 struct Rob { friend int A::*FiledPtr() { return M; } }; template struct Rob<&A::x_>; int main() { A o; o.*FiledPtr() = 10; assert(o.X() == 10); }这个方法是通过模板的显式实例化（Explicit Instantiation）实现的，因为在显式实例化的规则中有这样一个特例：The usual access checking rules do not apply to names in a declaration of an explicit instantiation or explicit specialization, with the exception of names appearing in a function body, default argument, base-clause, member-specifification, enumerator-list, or static data member or variable template initializer.1也就是说，当我们去做显式实例化的时候，实际上是可以合法访问某个class的private成员的。由此就催生了如上面代码所示的，对类的访问控制机制的打洞。按常理来说，这是一个巨大的隐患——因为我们可以合法的绕过访问控制了，而一般来说这种情况都是UB的2。那么，是什么需求使得标准不得不给予显式实例化以特权，哪怕是以给access control留下后门为代价呢？请看下面的例子：需求——导出显式实例化class Foo { private: struct Bar; template<typename T> class Baz { }; public: void f(); // does things with Baz<Bar> }; // explicit instantiation declaration extern template class Foo::Baz<Foo::Bar>;这是个很好的例子，给了我们对于需求的直观感受：当你需要导出一个显式实例化模板，而这个模板实例化涉及到私有成员的时候，访问控制和实例化的需求就产生了一个必然的冲突。而由于显式实例化是一个确实存在的现实需求，C++的做法从来就不是为了维护其他规则而抹杀实现合理需求的可能性。如果真的要去通过这样的方法对access control打洞，那么可能产生的后果应该由程序员自己来掌控。大佬的评价（翻译）Johannes Schaub是这样说的（甚至还给出了另外的方法）：但实际上，这并不表明c++的访问规则不可靠。语言规则的设计是为了防止意外的错误——如果你试图抢夺一个对象的数据，语言的设计不会采取很长的方式来阻止你。而著名的GotW中也讨论过这个问题，给出了这样的见解：以上给出了两个C++特性之间的有趣互动。访问控制模型和模板模型。事实证明，成员模板似乎隐含了 "破坏封装"的意思，因为它们有效地提供了一种可移植的方式来绕过类的访问控制机制。这实际上并不是一个问题。这里的问题是保护Murphy还是保护Machiavelli......也就是说，保护意外的滥用（该语言做得很好）与保护故意的滥用（这实际上是不可能的）。最后，如果一个程序员非常想颠覆这个系统，他就会找到一个方法，正如上面的例子1到3所证明的。这个问题的真正答案是。不要这样做！无可否认，甚至Scott Meyers也公开说过，有些时候，我们很想有一种快速的方法可以暂时绕过访问控制机制，比如为了在调试时产生更好的诊断输出......但这不该是你在编写代码时养成的习惯，而且它应该出现在你的开发环境的"警告行为"列表中。来自GotW的编码标准忠告：永远不要颠覆语言；例如，永远不要试图通过复制一个类的定义并添加一个友元声明来打破封装，或者提供一个模板成员函数的本地实例（GotW #76)总结来说，通过技巧来故意绕过某些控制永远都是可能的，标准和语法检查想要阻止的不是这样的行为，而是无意…