八哥之神后传【10】

高中宿舍

晚上,稣要以穿墙术潜入传说中的鬼楼。

宿舍楼的左半边住男生,右半边住女生,但男生这边的六楼是有人住的,而女生那边的六楼已经荒废一年多。

稣曾经仰慕的学姐就在女生宿舍六楼离奇地香消玉殒,稣一定要探查个明白,并为学姐报仇。这点,学妹也是真心支持的,因为这个穿墙术正是学妹提供的。

宿舍是个 S 型建筑,S 中间有铁门,分割男女两边。男女两边走廊互相平行,但不在同一条直线上,中间的铁门一直是锁着的,这样男女两边都看不见各自那边的走廊。不过稣用穿墙术从六楼中间的铁门穿过去,就能轻易地来到女六楼。

进入之后,才亲眼见证另一边的楼梯入口也特地封起来,所以女生确实也是无法进入六楼的。眼前的六楼简直是另一个世界,出乎意料的破败,走廊上很多灰泥、玻璃渣。稣小心躲避,进入 614,里面的床都被搬走了,地上有好几个打破的热水瓶和一些餐具。

曾经有一个流言:有一天半夜每个女六楼的房间里都有几个女生被“怼”附身,拿起热水瓶往其她女生脸上倒,最后还互相厮杀,死了大半,剩下的全疯了。稣是相信科学的人,怎么可能相信这种怼话?这大概就是一种集体催眠罢了,但究竟是谁有这么大本事呢?难道是医务室的心理医生?

思考时,稣却看到窗户的玻璃上贴着一张照片。靠近一看,拍的居然是食堂的厨房。怎么会有如此奇怪的照片呢?稣想到大家都喜欢在照片背后写备注,于是翻过来,竟看到学姐的字迹,写着:食堂不干净!稣大吃一惊,不干净是这个意思,还是那个意思?难不成真有怼?不可能,一定是在做梦!

啊!稣竟然想不起学姐叫啥名字?只记得学姐的学姐叫桂圆……而学姐好像叫什么香……她还有个舍友姓许,经常打扮得很拉风的,也一样记不起名字了!

破庙

吓醒。

走了一天,还好有这个破庙能休息一下,但是环境惨不忍睹,难怪做起噩梦。是不是盖的这些稻草生菇了,又真菌中毒?不管了,天还没亮,继续睡。

高中宿舍

嗯?稣又回来了!

在宿舍的床上醒来,就看到上铺的一只手伸出床沿,只是颜色不太对!这手都枯成深色了,吓死人啊!

呵呵,其他舍友都一样……全都成干尸了。要说稣还是因为信仰科学而吓不倒,甚至高中生涯的目标就是考上吓大。果断出门看看其他人,竟然唱起空城计?

澡堂厕所都没活人,其它房门都反锁的无法进入查探情况。宿舍门口的开水房也没人打水。

女生那边的门也是关着的,说明她们都没起来过,没人开门出来。

走去教室,一样空无一人。这究竟是怎么回事?

突然,稣远远地看到操场居然有个高个子自己一个人在踢足球!稣立刻边跑过去边对着他喊:同学,你知道学校怎么了吗?

那位同学把球踢向稣,稣身体向后一仰,本想用胸口停球,不料足球靠近后稣发现它黑乎乎的,赶紧躲闪。

黄言:小开?你刚刚是在模仿尼欧躲子弹吗?

圣小开:言兄,原来是你啊!

黄言:你也是怼吗?

圣小开:什么怼?怎么大家都变成干尸了?

黄言:哦,果然你宿舍也一样。全挂了!

圣小开:你是说,咱俩也都挂了?现在是怼?

黄言:是的。

圣小开:不可能,其他人怎么没有变成怼?而且稣起床后,看过自己的床上,并没有尸体。

黄言:我床上有自己的尸体。

圣小开:你看见了?

黄言:我可以带你去看看。

圣小开:不了。怕怕。你知道是啥导致的吗?

黄言:可能是中子弹,或者声纳武器,也可能是生化武器。

圣小开:开啥玩笑呢?这和平年代的,谁会在学校扔武器?

黄言:并不是只有学校哦,我出去看过了,整个城都是这样。你还记得我写的小说吗?

圣小开:你又想说你化身的主角在山洞里发现希特勒留下的财富?

黄言:这不是重点,你记得看完那小说后,你说了啥?

圣小开:稣很担心有生之年会遇到第三次视觉大战。是这句?

黄言:是的。

圣小开:稣担心的往往会变成现实……

黄言:没错!自从你跳楼后,视界就走向毁灭的边缘。

圣小开:稣不是又复活了吗?

黄言:我听说你被送医后又活了过来,但再也没见过你,料想一定是假的,哪有人从六楼跳下来还不死的?

圣小开:呃!说了你也不信,其实这个视界真的是虚拟的,它们想从稣的意识深处拿到某种机密,所以不断复活稣。

黄言:我信你。个怼。你能有啥机密?说出来吓吓我!

圣小开:呵呵,你以为变出个熟人,稣就放松警惕了吗?稣还能吓醒。

黄言:哈哈,你去未来看看吧!

2011-01-24 软件园非正常人类研究中心

又是一个早上醒来,原来稣在软件园非正常人类研究中心上班。嗯,昨天一切都还正常,也不知道那个昨天是不是被植入的记忆。总之今天出现了一系列怪异的事情,去买早点没遇到半个人,当然整个的人也没有,到了早餐店才发现没开门。稣想:算了,公司还有点东西可以撑一下肚子。

来到公司才发现更怪异的事,人倒是有,但都是骷髅。真佩服自己当时的淡定,居然没有大呼小叫神马的!不过当时马上想到世界末日,于是看看手机,时间是 2011 年,还早呢。好奇地打电话给家里,他们都表示世界没有抛出任何异常。然后又想:惨了,不会是只有软件园发生灾难吧!稣的弟弟也在软件园,马上打电话给他,他说公司送他们项目组的几个去北京玩了,不知道软件园发生了神马!

接着,发现脑子里还有一个人,可能又是被植入的女朋友,吧?就硬着头皮,往那处走过去,人不在位置,不过那屋子里的其他位置一样坐着几个骷髅,心想好像稣在意的人都没事,真是不幸中的万幸。

为了确保万无一失,稣给她打电话,没人接,不过不接不一定就是挂了,但是稣还不放心,到车库去看了一下,熟悉的车牌号,跑过去,震惊了……车里一个骷髅……车门打不开……过了一会儿,稣马上就冷静了,发现那个骷髅比她大了好几个尺寸,sigh~应该不是!

这时有人叫稣,一转身,居然是黄言!

黄言:都看见了?这视界总是要走向毁灭,躲得了初一,躲不过十五。

圣小开:稣一定要阻止战争!吓醒。

诗盗·大禹

《诗盗·大禹》:治水为命顺逆同,浩然莫测鬼神工。三过涂山生悟空,定海神针串鱼龙。

注解

改自霹雳角色“渊渟无迹静涛君”的诗号:

知水为命顺逆同,
浩然莫测深浅中。
无波沧海掩汹涌,
渊渟不动现鱼龙。

指针判空

引子

C 和 C++ 都有一些很基本的语句出现不同派系的写法,比如 * 靠左还是靠右,抑或居中?这种还是排版问题,并不影响有效字符数,但下面这个就直接影响有效字符数了!

《对于选择恐惧症患者来说,指针判空究竟要怎么写才不纠结?》

问题

指针判空有两大类写法:

  • if (p) {}

  • if (nullptr != p) {}

后者被挺多人推荐的,比如林锐的《高质量C++/C编程指南》。

高质量C++/C编程指南

搜一下,能发现不少网友都挺纠结。这么基础的问题,如果不交代清楚,就是给 C++ 黑很好的攻击理由。

故事

稣刚大学毕业时,是使用 C,并不屑 C++ 的,当时看了不少代码,就是直接 if (p)if (!p),所以也坚持这种写法,并认为这样写比较短,对手指好,比较养生。

后来 C++17 出现,稣就坚定地改用 C++,于是也更多遵守 C++ 类型安全的原则,开始认为 if (p)if (!p) 这种写法隐含类型转换,不太好。记得 2017 年以前,其实就看过 Google C++ Style Guide,里面也曾经建议写成 if (nullptr != p)if (nullptr == p)

现在,再去看 Google C++ Style Guide,已经没有这样的建议。

解决

C++ Core Guidelines 中有一条:ES.87: Don’t add redundant == or != to conditions,它的理由是:

Reason Doing so avoids verbosity and eliminates some opportunities for mistakes. Helps make style consistent and conventional.

if、while、for 的条件语句本身就是在选择 true 或 false,对于指针来说,会自动与 nullptr 比较。通常来说,if (p) 可以读作 “if p is valid”。

接着,还有一个例子:

1
2
3
if (auto pc = dynamic_cast<Circle>(ps)) { ... } // execute if ps points to a kind of Circle, good

if (auto pc = dynamic_cast<Circle>(ps); pc != nullptr) { ... } // not recommended

同样是推荐更简短的写法。但这个例子却有人提出疑问:推荐的写法不会被怀疑是 == 少写了一个 = 吗?

还真不会!这个语句其实不是经典 C++ 的语法,而是 C++17 的 init-statement 语法,这里的 auto 是类型,说明 = 不可能是 == 错误地少写成 =。

对于部分 C 程序员和“经典 C++”程序员会把赋值语句写在 if 条件里的做法,建议是改为分开写。

1
2
3
4
5
6
7
8
// p is defined before

if (p = func()) { ... } // bad

p = func();
if (p) { ... } // good

// p is used after

稣会在新工程里坚持使用 C++ Core Guidelines 的建议,但也不反对另一种写法,只要不混合使用。允许有不同派系,但最好别精神分裂。

BoringSSL? LibreSSL? OpenSSL!

问题

  • Windows 自带 OpenSSH 用的是 LibreSSL。要不要也用一下?甚至,直接动态链接到系统自带的 LibreSSL 的 dll,减少 exe 体积!

  • Chrome 用的是 BoringSSL,要不要……折腾一下?

尝试

稣使用 vcpkg,安装几个库,还不是手到擒来?

结果……连装都不让装!这是 LibreSSL 的:

1
2
3
4
5
Building libressl:x64-windows...
CMake Warning at ports/libressl/portfile.cmake:2 (message):
Can't build libressl if openssl is installed. Please remove openssl, and
try install libressl again if you need it. Build will continue since
libressl is a subset of openssl

这是 BoringSSL 的:

1
2
3
4
5
Building boringssl:x64-windows...
CMake Error at ports/boringssl/portfile.cmake:2 (message):
Can't build BoringSSL if OpenSSL is installed. Please remove OpenSSL, and
try to install BoringSSL again if you need it. Build will continue since
BoringSSL is a drop-in replacement for OpenSSL

在忍痛 vcpkg remove openssl 后,陷入一阵思考——稣主要通过 Boost.Asio 使用 SSL,所以问题转换为:Boost.Asio 对这几个 SSL 库支持得如何?其中,OpenSSL 是使用多年没有任何问题的,只需要调研其它两个!

先试试 LibreSSL,毕竟是 OpenBSD 的,而且微软也用它。拿 ClipboardSync 代码编译,发现顺利通过!但是运行时抛了异常,说不支持 TLS 1.3……遂查阅官网,说是从 3.2.0 就支持!那就是 Boost.Asio 不对了,果断给它提 issue

接着尝试 BoringSSL,毕竟是 Google 的,号称重视安全,而且有 Chrome 这个大型流行软件做背书!然而很打脸的是:它居然不支持 SM3!稣当年特地选择用国密标准里的 SM3 做 Hash 算法,就是因为爱国!不支持国密这点岂能忍?立刻 vcpkg remove boringssl

结论

Boost.Asio 和稣联合推荐 OpenSSL 为唯一好用又爱国的 SSL 库。

Boost【9】Boost.Beast

介绍

Boost.Beast 是一个 HTTP/WebSocket 库。本文只讨论 WebSocket。

相关经验

UMU 先是用过 WebSocket++(websocketpp),又用过 libwebsockets,再用的 Boost.Beast,之后就一直使用 Boost.Beast。

  • 2018 年,参与 EOS 开发时,它是用 WebSocket++ 的,跟着学习了一阵子。

  • 2020 年,在金山云时,内部版云游戏用 libwebsockets,跟着学习了一阵子。

  • 做开源版云游戏——鎏光云游戏引擎时,特地学习并使用 Boost.Beast。因为公司其实是要求和内部版有一些差异的,正好之前一直用 Boost.Asio,对它的熟悉可以快速套用在 Boost.Beast,于是果断切到 Boost.Beast。

什么场景应该使用 WebSocket?

如果您原本使用裸 TCP,您应该知道 TCP 的流式传输,导致您需要自己分包,即界定一个“消息”的边界。鎏光本来设计为同时支持裸 TCP 和 WebSocket 的,所以还保留着处理分包的代码:

https://github.com/UMU618/liuguang/blob/97f558275571b9be14893e4b55703b4b65cdbda5/src/cge/cge/game_session.cpp#L232

这对 WebSocket 其实并不需要,它的发送和接受已经是都是“消息”,带着长度的。所以如果您原来用 Asio 写 C/S 程序,把它们改为 Beast,是很容易的,而且代码量会缩减不少。

对于工具性的 C/S 程序,建议下次直接用 Beast 写更省事。

另一种适用场景是需要支持浏览器,即同时支持 C/S 和 B/S 模型。

Boost.Beast 经验

  1. text 和 binary 模式需要区分清楚,如果用于发送音视频,显然应该使用 binary 模式。

  2. stream 的默认接收长度是 16MiB,如果不够可以改长点,0 表示最大的 std::uint64_t。

参考:boost/beast/websocket/stream.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** Set the maximum incoming message size option.

Sets the largest permissible incoming message size. Message
frame fields indicating a size that would bring the total
message size over this limit will cause a protocol failure.

The default setting is 16 megabytes. A value of zero indicates
a limit of the maximum value of a `std::uint64_t`.

@par Example
Setting the maximum read message size.
@code
ws.read_message_max(65536);
@endcode

@param amount The limit on the size of incoming messages.
*/
void
read_message_max(std::size_t amount);

/// Returns the maximum incoming message size setting.
std::size_t
read_message_max() const;
  1. 默认开了 deflate 压缩。UMU 在开发 ClipboardSync 时,曾经考虑给剪切板数据压缩,对比了 lz4 和 zstd,很是犹豫,后来发现 Beast 默认开了 deflate 压缩,于是放弃自己用 lz4 或 zstd 压缩。

自动添加 IP 黑名单到防火墙

问题

前文《防爆破远程桌面密码》提到可以用 Powershell 实现自动加 IP 黑名单到防火墙,这个坑还是得填,毕竟爆破依然在持续……

分析

核心点:

  • IP 黑名单会持续新增,过去已经加入防火墙的名单也需要保存。所以应该把新增 IP 和防火墙已有 IP 求并集。

  • 如果名单没变,不应该覆盖防火墙规则。

代码

见 Github 仓库:

https://github.com/UMU618/windows-scripts/blob/master/pwsh/add-ip-blacklist-to-firewall.ps1

防爆破远程桌面密码

问题

面向公网开了个远程桌面端口,无论用多少号端口,都会被爆破!

虽然稣的密码很安全,几乎爆破不了,但会在系统日志里留下大量记录,实在很不雅观!

分析

改端口是没用的,因为只有 65535 个,只要机器 IP 被发现,扫描端口很快就能完成。

封 IP 是有用的,虽然爆破者(攻击者)有很多 IP,但一定是有限的,有多少封多少!

解决

  1. 从系统日志里分析爆破者 IP

打算使用 PowerShell 7 来编写脚本,首先学习 Get-WinEvent 命令:

1
Get-Help Get-WinEvent -Online

日志的过滤条件可以用“事件查看器”来协助生成:

WinEvent UI
WinEvent XML

当然,以上全部 XML 是 -FilterXml 的参数,比较长,可以用 -FilterXPath 来简化,只需要中间一部分。

条件还可以再加上 LogonType,以缩小范围,其中 3 表示“网络登录”:

1
Get-WinEvent -LogName 'Security' -FilterXPath '*[System[EventID=4625] and EventData[Data[@Name="LogonType"]=3]]' -MaxEvents 1 | Format-List Message

以下是完整代码,它会打印出 IP,和这个 IP 的登录失败次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ips = @{}
Get-WinEvent -LogName Security -FilterXPath '*[System[band(Keywords,4503599627370496)] and EventData[Data[@Name="LogonType"]=3]]' | %{
$xml = [xml]$_.toXml()
foreach ($data in $xml.Event.EventData.Data) {
if ($data.Name -eq 'IpAddress') {
$ip = $data.'#text'
if ($ips.ContainsKey($ip)) {
++$ips[$ip]
} else {
$ips.Add($ip, 1)
'Fisrt find ' + $ip + ' on ' + $_.TimeCreated
}
}
}
}
Write-Output 'All IPs:', $ips
  1. 把爆破者 IP 加入防火墙,阻止它们

目前收集到这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
112.184.96.197
118.45.92.239
122.38.193.166
138.199.21.245
14.116.196.99
14.49.207.162
141.98.11.119
141.98.11.58
147.78.47.57
148.113.4.245
176.111.174.173
176.111.174.174
179.60.147.13
182.180.92.224
185.161.248.145
188.250.64.50
222.191.242.228
45.143.201.62
58.221.4.54
58.33.52.84
62.122.184.88
67.159.237.58
77.90.185.132
78.128.114.18
89.248.163.94
89.248.163.95
91.191.209.202
91.240.118.187
91.240.118.29

打开 wf.msc,新建一个阻止型的防火墙策略,然后加入到“作用域”的“远程 IP 地址”里。

Firewall

加防火墙也可以用 PowerShell 搞定,这次先偷个懒,下次再说吧!

函数名首字母用大写还是小写?

问题

不同编码规范对函数名的命名格式有不同要求,但主流有以下几类:

哪种适合 Windows 开发呢?

分析

1. PascalCase 的坑

PascalCase 偶尔会遇到和 Win32 API 宏冲突的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <Windows.h>

#include <iostream>

namespace umutech {

int CreateWindow() noexcept {
MessageBox(nullptr, L"For disassembling", L"CreateWindow", MB_OK);
return 0;
}

} // namespace umutech

int main() {
umutech::CreateWindow();
}

以上代码无法编译,因为 SDK 头文件里有这样的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define CreateWindowA(lpClassName, lpWindowName, dwStyle, x, y,\
nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam)\
CreateWindowExA(0L, lpClassName, lpWindowName, dwStyle, x, y,\
nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam)
#define CreateWindowW(lpClassName, lpWindowName, dwStyle, x, y,\
nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam)\
CreateWindowExW(0L, lpClassName, lpWindowName, dwStyle, x, y,\
nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam)
#ifdef UNICODE
#define CreateWindow CreateWindowW
#else
#define CreateWindow CreateWindowA
#endif // !UNICODE

导致以下编译错误:

1
2
3
4
5
6
1>function_name.cpp(7,5): warning C4003: not enough arguments for function-like macro invocation 'CreateWindowW'
1>function_name.cpp(7,5): error C2059: syntax error: ','
1>function_name.cpp(7,29): error C2143: syntax error: missing ';' before '{'
1>function_name.cpp(7,29): error C2447: '{': missing function header (old-style formal list?)
1>function_name.cpp(15,12): warning C4003: not enough arguments for function-like macro invocation 'CreateWindowW'
1>function_name.cpp(15,12): error C2059: syntax error: ','

改成下面这样,才能编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <Windows.h>

#include <iostream>

namespace umutech {

int CreateWindowEx() noexcept {
MessageBox(nullptr, L"For disassembling", L"CreateWindowEx", MB_OK);
return 0;
}

} // namespace umutech

int main() {
umutech::CreateWindowEx();
}

以上代码虽然编译通过,但实际上 CreateWindowEx 还是个宏。用 IDA 逆向编译后的 exe,并加载 pdb 后,可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; int __cdecl main(int argc, const char **argv, const char **envp)
main proc near
sub rsp, 28h

; int __fastcall umutech::CreateWindowExW()
umutech__CreateWindowExW: ; uType
xor r9d, r9d
lea r8, Caption ; "CreateWindowEx"
lea rdx, Text ; "For disassembling"
xor ecx, ecx ; hWnd
call cs:__imp_MessageBoxW
xor eax, eax
add rsp, 28h
retn
main endp

虽然没啥危害,但 C++ 20 程序员不喜欢宏!

2. camelCase 是不是更好?

如果函数名使用 camelCase,则没有机会与 Win32 API 的宏定义冲突。

另一个好处是在做 API Hooking 时,命名可以更短。比如 Hook ShowWindow,那么替代函数可以就叫 showWindow,而用 PascalCase,则可能需要叫 MyShowWindow。

那么是不是把 Google C++ Style 的函数名由 PascalCase 改为 camelCase 就完美了?

更好,并不是完美……camelCase 也有个小问题——只有一个单词时,无法区分是 camelCase,还是 snake_case。比如 size,是函数(camelCase),还是临时变量(snake_case)?

总结

UMU 建议,如果已经在使用 Google C++ Style,应该避免函数名与 Win32 API 一样。如果正在从头制定一套 Coding Style,则可以考虑函数用 camelCase。

解决运行 bcdedit 后 BitLocker 提示输入恢复密钥

问题

因为种种原因……需要调试本机内核,用 bcdedit 开启调试模式:

1
bcdedit -debug ON

结果重启后,BitLocker 提示输入恢复密钥!

分析

首先,要强调——调试本机内核,本身就是一个很危险的操作!建议还是用虚拟机调试。

其次,您一定已经备份了 BitLocker 的恢复密钥。只是它不一定在身边,比如说放在家里,人在公司,一来一回需要很长时间,所以得想办法节省时间。

最后,不要被 BitLocker 吓倒!即使,您没有备份恢复密钥,也还有救!在这个界面按 ESC,再点“跳过此驱动器”,后面是可以进入“控制台”的,只需要在“控制台”里撤销操作即可!

解决

尝试关闭调试模式:

1
bcdedit -debug OFF

先别急着重启,因为这么敲——无效!不信您可以重启后再运行 bcdedit,会发现这个 debug 选项还是 Yes。

看来在 WindowsRE 环境下,直接运行 bcdedit,并不能修改 C 盘里的启动选项。您需要加上个 ID,一般为 {default}。

1
bcdedit -debug {default} OFF

先别急着重启,因为这么敲——还是无效!原本没有 debug 这个值,现在多了一个,数据是 No 而已,因为默认值就是 No,看似没有改变“调试模式”,但其实 BCD 数据库是变了的。正确的做法是删除这个 debug 值:

1
bcdedit -deletevalue {default} debug

重启后不再要求输入恢复密钥。这时可以在 BitLocker 的控制面板里先暂停保护,然后再操作 BCD,即可。

为什么应该使用 C++ 20?

实战项目

  1. 鎏光云游戏引擎

  2. 金山云 LiveNet

  3. ClipboardSync

  4. PowerEconomizer

其中,鎏光一开始是用 C++ 17 的,在 MSVC 的 std::format 可用时,第一时间切换到 C++ 20。而 LiveNet 主要运行于 Linux,开发时 gcc 还不支持 std::format,使用 fmt::format 代替,但一直用 cxxstd=20 编译。

ClipboardSync 和 PowerEconomizer 使用了 C++ 20 modules。

现在还在开发的云桌面产品也是使用 C++ 20,但没有用 modules。

理由

根据 jetbrains 的统计,2022 年时,C++ 20 的使用率是 23%,已经超过经典 C++ 的 8%。在游戏开发领域,C++ 20 的使用率为 25%,甚至已经超过 C++ 14 的 24%。

以下列举能够很容易想到的一些好处:

  1. 好用、安全的新类:std::format、std::span、std::jthread、原子(Atomic)智能指针

  2. designated-initializers 安全初始化,防止因为调整结构体而顺序不对

  3. modules 加速编译

  4. 更多标签 [[likely]], [[unlikely]], [[nodiscard(reason)]]

  5. 可以 using enum

  6. 对模板形式的 Lambda 有更好支持

  7. 范围 for 循环支持初始化

  8. 三路比较运算符 <=>

  9. Boost 的 awaitables 协程(BOOST_ASIO_HAS_CO_AWAIT)需要 C++ 20

  10. ranges 库

阵痛

  1. 曾经遇到 clang-format 对 modules 支持不好的问题,后来升级 clang-format 解决。但目前还不建议在大型项目里使用 modules。

  2. 有些隐式转换无法编译,尤其在编译驱动代码时,容易遇到连 WDK 里的头文件都无法编译。这是因为 C++ 20 比 C++ 17 都严格。建议内核态驱动使用 C++ 17;用户态驱动可以 C++ 20。