解读一份极其晦涩的c++代码
事先声明,下面的各种语法技巧都是炫技式的,刻意降低代码的可读性,只是写着玩,在实际应用中是不会允许这么乱写的。
先来几个开胃菜。
开胃菜
(1) 嵌入网址?
下面这段代码看起来是在c++中直接嵌入了一个网址 1
2
3
4
5
6
7
int main(){
https://www.zhihu.com
std::cout << "Hello World!" << std::endl;
return 0;
}
它确实是合法的c++代码,但是含义却并不是网址:
- 左半部分
https:
是一个标签,可以通过goto
跳转; - 右半部分由于
//
的存在,就是一个注释,直接被忽略了。
下面的代码就更利于理解了 1
2
3
4
5
6
7
8
9
int main() {
label: // 这是注释
std::cout << "Hello World!" << std::endl;
goto label;
return 0;
}
这里加上一个goto
就变成了一个死循环,程序会不断输出Hello World!
。
(2) 各种括号乱排?
下面这段代码是啥? 1
2
3
4int main(){
[](){}();
[]{}();
}
这仍然是一段合法的C++代码,虽然没有任何实际意义:
- 第一句的含义是定义了一个不捕获任何变量(
[]
),且不接受任何参数(第一个()
代表形参列表为空)的lambda表达式,并且函数体为空({}
),无返回值,然后直接调用了这个lambda表达式(第二个()
代表函数调用)。 - 第二句是在第一句的基础上,进一步省略了形参列表。
下面的代码就更利于理解了 1
2
3
4
5
6
7
8
9
10
11int main(){
auto f1 = [](){
// ...
};
f1();
auto f2 = [](){
// ...
};
f2();
}
目标
我们的目标是解读下面这份代码 1
2
3
4
5
6
7
8
9auto main() -> decltype('O.o') try
<%[O_O = 0b0]<%
https://www.zhihu.com/question/37692782/answer/112123204607
typedef struct o O;
o*(*((&&o(o*o))<:'o':>))(o*o);
if(O*O = decltype(0'0[o(0)](0))(0)) 1,000.00;
else return 0==O==0 ? throw O_O : O_O;
%>();%>
catch(...) { throw; }
这份代码非常晦涩,可读性几乎没有,但它确实是合法的现代c++代码,可以顺利通过编译,顺利运行,运行过程没有任何输出。
准备
为了读懂目标代码,我们需要进行一些准备。
(1) 双元符(Digraphs)
在早期可能是为了兼容一些打不出中括号和大括号的输入设备,C++支持在代码中使用如下的特殊字符组进行替代的方案,称为双元符(Digraphs)
1
2
3
4
5
6<% is {
%> is }
<: is [
:> is ]
%: is # 预处理指令(如 #include)
%:%: is ## 预处理运算符(宏拼接)
编译器在处理时会将其直接替换为对应的字符,例如 1
2
3
4
5
6%:include <iostream>
int main() <%
int a<:4:> = <%1,2,3,4%>;
std::cout << "Hello, World!\n";
%>
完全等价于 1
2
3
4
5
6
int main() {
int a[4] = {1,2,3,4};
std::cout << "Hello, World!\n";
}
除了双元符,其实还有三元符(Trigraphs),例如
??=
相当于#
,但是三元符已经在C++17中被移除,双元符仍然被保留了下来。
(2) 函数try块
在main函数中可能经常看到这样的异常捕获语句 1
2
3
4
5
6
7
8
9
10int main() {
try {
// ...
return EXIT_SUCCESS;
}
catch (const std::exception &e) {
std::cout << e.what() << std::endl;
return EXIT_FAILURE;
}
}
c++其实还支持下面这种写法,效果是完全一样的 1
2
3
4
5
6
7
8int main() try {
// ...
return EXIT_SUCCESS;
}
catch (const std::exception &e) {
std::cout << e.what() << std::endl;
return EXIT_FAILURE;
}
函数try块 这个语法的引入主要是为了解决构造函数无法捕获初始化列表中抛出的异常的问题。不过既然引入了新语法,并不局限于构造函数,一般的函数(包括main函数)也都可以用,虽然没什么意义。
(3) decltype的一个细节
下面这三个变量的类型都是char
吗? 1
2
3decltype('a') x;
decltype('ab') y;
decltype('abc') z;
实际上:
x
的类型是char
;y
和z
的类型是int
。
看着有点奇怪,但是语法就是这么规定的,不解释。
(4) 类型与变量重名
一个容易忽略的事实是:虽然int
等基本类型是保留关键字,我们无法定义名为int
的变量,但是对于其它自定义类型,C++是允许变量名和自定义类型名重复的,编译器可以正确地区分它们。
例如下面的代码是合法的,可以通过编译且正确运行 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct demo {
int x = 10;
void hello() { std::cout << "Hello!\n"; }
static void hi() { std::cout << "Hi!\n"; }
};
int main() {
demo demo;
demo.hello();
demo.hi();
demo::hi();
demo.x = 10;
return 0;
}
(5) 类型与函数重名
除了自定义类型可以和变量重名,在某些情况下自定义类型还可以和函数重名,例如
1
2
3
4
5
6
7
8
9
10
11
12struct o{
int s;
};
o *o(o *){
return 0;
}
int main() {
o(0);
return 0;
}
具体哪些情况下允许我也不知道,但是至少不是绝对禁止的,如果编译器可以分得清,也是可以的。
解读main函数
有了前面的准备,现在我们开始正式解读这份难倒deepseek的代码
1
2
3
4
5
6
7
8
9auto main() -> decltype('O.o') try
<%[O_O = 0b0]<%
https://www.zhihu.com/question/37692782/answer/112123204607
typedef struct o O;
o*(*((&&o(o*o))<:'o':>))(o*o);
if(O*O = decltype(0'0[o(0)](0))(0)) 1,000.00;
else return 0==O==0 ? throw O_O : O_O;
%>();%>
catch(...) { throw; }
首先最明显的就是采用了一些双元符进行混淆,将其替换就可以得到下面的代码
1
2
3
4
5
6
7
8
9auto main() -> decltype('O.o') try
{[O_O = 0b0]{
https://www.zhihu.com/question/37692782/answer/112123204607
typedef struct o O;
o*(*((&&o(o*o))['o']))(o*o);
if(O*O = decltype(0'0[o(0)](0))(0)) 1,000.00;
else return 0==O==0 ? throw O_O : O_O;
}();}
catch(...) { throw; }
接下来我们可以进行几项简化:
https:
开头的一行明显就是一个注释加上label,而且代码中的其它地方并没有出现goto
或者其它跳转语句,因此直接删掉即可decltype('O.o')
的结果是int,直接替换为标准的int main()
即可
然后顺便让 clangd
进行了代码格式化,调整一下缩进,就可以得到下面的版本 1
2
3
4
5
6
7
8
9
10
11
12
13int main() try {
[O_O = 0b0] {
typedef struct o O;
o *(*((&&o(o * o))['o']))(o *o);
if (O *O = decltype(0'0 [o(0)](0))(0))
1, 000.00;
else
return 0 == O == 0 ? throw O_O : O_O;
}();
}
catch (...) {
throw;
}
到这里为止,大致内容已经比较清晰了:
- main函数使用了函数try块的语法,套了一层try-catch,里面确实有分支进行了throw,所以还得保留;
- main函数中只有一个语句,它定义了一个lambda表达式并且立即调用了它;
- 理解一下这个lambda表达式:
- 捕获列表中定义了一个变量
[O_O=0b0]
,也就是一个int类型的变量O_O
,值为0; - 形参列表为空,因此直接省略了
()
; - 函数体比较复杂,后面再说。
- 捕获列表中定义了一个变量
我们只需要弄懂lambda表达式的函数体即可。
解读lambda表达式
将lambda表达式的函数体拎出来进行整理,不妨给if语句加上括号,并且不妨把捕获变量也写成普通的局部变量形式,可以得到如下的代码块
1 | int O_O = 0b0; // (1) |
其中:
- 是非常简单的,定义
int
类型的变量O_O
,赋初值0;
- 是非常简单的,定义
- 给一个结构体类型
struct o
定义了别名O
,由于并没有提供定义,这个结构体无法实例化,在代码中最多只能使用指针类型struct o *
等;
- 给一个结构体类型
- 是第一个难啃的骨头,这里要么是一个极其复杂的函数声明,要么是一个极其复杂的变量定义;
- (4)是一个if条件判断
- (4.1)
在if语句中定义了一个类型为
O *
(也就是struct o *
)的指针变量O
,注意它和自定义的类型别名出现重复了,但是这在语法上是允许的;给变量O
赋初值的一大堆内容是第二块难啃的骨头 - (4.2) 是if条件满足时执行的语句,但是它只是一个语句,没有返回值,也不产生什么实际效果
- (4.3)
是多个
==
和三元运算?:
的嵌套,涉及到了O
和O_O
这两个变量,只需要明确一下语法优先级即可
- (4.1)
在if语句中定义了一个类型为
(4.2)
的解读:,
作为二元运算符,a,b
这个表达式的结果就是b
1
1, 000.00;
下面这种才是c++支持的数字分隔符 1
10'000;
(4.3)
的解读:?:
的优先级最低,==
的结合顺序是从左到右,因此可以写成
1
return ((0 == O) == 0) ? (throw O_O) : O_O;
解释为:
- 如果
0 == O
返回true,true和0比较得到false,最终返回0; - 如果
0 == O
返回false,false和0比较得到true,最终抛出0。
这个lambda表达式在if语句的if分支中不会返回任何内容,在else分支中则可能抛出0或者返回0,lambda表达式的返回类型为int。
解读最晦涩的部分
现在我们来解读lambda表达式中最晦涩的两个语句:
1 | typedef struct o O; // (H0) |
这里的困难来自几个方面:
- 复杂的函数指针定义或者函数类型声明本身就很难读懂;
- 这里的符号出现了大量的
o
,既可能是之前声明的结构体类型o
(c++允许省略struct
),也可能是引入的新变量或者新声明的函数,还可能是无意义的占位参数,甚至字符'o'
也拉出来凑数了; - 出现了太多的0,0可能是int类型的数,也可能是空指针,还可能是索引等。
我们先进行一点简化,首先把['o']
整理为[111]
,把0'0
整理为0
,得到
1 | typedef struct o O; // (H0) |
由于 (H2) 是合法的语句,这表明 (H1) 必然引入了一个名为 o
的函数或者变量, 否则单靠 (H0) 给出的类型声明是不可能让 (H2)
通过编译的。
这里我们断定 (H1) 中的两处 o * o
的第二个 o
都是没有意义的,可以直接删掉,得到 1
o *(*((&&o(o *))[111]))(o *); // (H1)
观察可知 &&o
这里的 o
才是最核心的标识符,其它几个只是用来组成指针类型 o *
而已。
接下来就是一层层扒开即可,从外往里可以读到一个参数类型为
o *
,返回值类型也为 o *
的函数指针,使用别名可以整理为
1 | typedef o *(*Fp)(o *); |
再拆一层,从外往里可以读到一个参数类型为
o *
,返回值类型为Fp (&&)[111]
(数组的右值引用类型)的函数,因此可以进一步整理
1
2
3
4
5
6
7
8typedef o *(*Fp)(o *);
typedef Fp (&&Ret)[111];
// or
using Fp = o *(*)(o *);
using Ret = Fp (&&)[111];
Ret o(o *);
最后剩下的就是函数声明:函数的名称是o
(和自定义类型重名了,但是这里允许重名),它的参数类型为
o *
,返回值类型为 Ret
。
小结一下:
o
是这样的函数:输入是o *
类型,输出是Ret
类型;Ret
是长度为111的Fp
数组(函数指针数组)的右值引用类型;Fp
是一个函数指针类型,指针指向的函数输入o *
类型,输出o *
类型;
可以用 (H2) 验证一下我们对 (H1) 的解读 1
2
3typedef struct o O; // (H0)
O *O = decltype(0[o(0)](0))(0); // (H2)
其中:
o(0)
:给函数o
传入了空指针作为参数,得到的返回值类型是Ret
;0[o(0)]
(这里[o(0)]
先和左侧的0
结合):实质即*(o(0)+0)
,也就是取了一次地址,得到Fp
数组的第一个元素;0[o(0)](0)
:Fp
数组的第一个元素是一个函数指针,不需要解引用,可以直接传入一个空指针作为参数,得到的返回值类型为o *
最终
decltype(...)
得到的结果是o *
类型,因此将0
显式转换为o *
类型的指针,正好给O *
类型的变量O
赋初值。
因此我们顺利解读了 (H1) 和 (H2) 的内容,并且从 (H2) 的解读可知:
- if语句中定义
O *O = ...
的结果为false,进入else分支; - 在else分支中,
0 == O
的结果应该为true,返回0
因此lambda表达式最终会返回0。
重写
解读完成之后,顺手将这份可读性极地的代码用更友好的方式完全重写一遍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25int main() try {
auto f = [/* int */ a = 0] -> int {
https: // www.zhihu.com/question/37692782/answer/112123204607
struct demo;
using Demo = demo;
using Fp = demo *(*)(demo *);
using Ret = Fp(&&)[111];
Ret fn(demo *);
if (Demo *b = decltype((fn(nullptr)[0])(nullptr))(nullptr)) {
(1, 000.00); // no return
}
else {
if (nullptr == b) { return a; }
throw a;
}
};
f();
}
catch (...) {
throw;
}