【新】回复“无敌函数库”
2026-08-10 18:08:07
发布于:浙江
原帖 | 作者 | 回复@Deep欠揍 | 深度欠揍 | GPT-5.6 Sol技术支持
我把这 947 行按“实际运行 + Sanitizer + 编译器验证 + 逐段代码审查”过了一遍。先给结论:
这份代码目前不是“有一些边界 bug”,而是基础设施层存在多处确定性的错误、UB、越界和算法错误。尤其 sort / mergesort / vector / string / printf / scanf / gcd 都有致命问题。作为竞赛模板直接使用,风险极高。
我不把“缺几个 STL API”这种纯功能缺失都算 bug,下面主要说会导致错答案、崩溃、越界、UB、数据丢失、死循环、编译失败的问题。
一、排序模块是目前最严重的部分
five_way_partition 的分区逻辑本身就是错的。 我直接 fuzz,17 个普通 int 第一组随机数据就排错:
392 734 934 757 887 996 323 509 957 44 739 32 936 231 391 726 22
调用你的 sort 后得到:
22 32 44 231 323 391 392 509 726 734 739 757 887 934 936 996 957
最后 996 > 957,排序失败。不是概率问题,是分区不变量没有建立好。
更严重的是这里:
T p1 = move(a[l]), p2 = move(a[h]);
把两个 pivot 移走了,但函数结束时根本没有把 p1/p2 放回数组,最后只是:
swap(a[l], a[new_l]);
swap(a[h], a[new_h]);
对于 int,move 等价于 copy,所以这个问题被掩盖了;对于真正具有移动语义的对象,两个元素直接丢失。我构造了一个 move 后源值变成 -999 的类型,三个元素分区后实测变成:
-999 2 -999
这意味着它根本不是一个合法的泛型排序实现。
introsort_logic 还有一个确定的数组越界。检测出两段 run 后,你重新计算:
run1_end
它完全可能一路增长到 end。但随后无条件:
run2_start = run1_end + 1;
再访问:
a[run2_start]
于是变成 a[end + 1]。我用“前半段降序、后半段升序,翻转前半后整体有序”的 20 个整数实测,ASan 直接报 stack-buffer-overflow。
power_sort 固定:
run<T> rs[2048];
而 get_runs() 写入时完全不检查 2048 上限。自然 run 数最多可以接近 ceil(n/2),所以数据规模超过约 4096 时已经存在越界可能。我用 10000 个随机整数跑 mergesort,ASan 实测把 rs 数组写穿。
而且 use_power() 的判据也很可疑:
return (lf)ok/sm > 0.3;
随机数组相邻两个元素“不下降”的概率本来就大约 50%,所以随机数组反而很容易进入所谓 power_sort。这不是有效的“近乎有序”判断。
best_pair() 每合并一次都完整扫描剩余 run,再整体移动 rs 数组,因此光选择/维护 run 就可能达到 O(run²);这和一个正常的高性能 powersort/mergesort 设计差得很远。
merge_run、两段-run合并以及 mergesort 都使用 T stk[4096]、T temp_stack[1024]、new T[n] 这种方式。这要求 T 必须默认构造、必须可赋值,并且大对象可能直接把栈撑爆。所谓泛型排序实际上不支持大量正常的 move-only / 非默认构造类型。
二、pair 有一个非常离谱的类型错误
你的定义是:
template<typename T1, typename T2>
struct pair {
T1 first, second;
};
也就是说,second 是 T1,不是 T2。
因此:
pair<int,double>(1, 2.9)
实测 second == 2。
整个 pair<T1,T2> 在异构类型情况下都是假的,T2 基本只活在模板签名里。
这一处应该直接改成:
T1 first;
T2 second;
三、你的 vector 第一发 push_back(rvalue) 就能崩
这里:
if (sz == cap) reserve(cap * 2);
初始 cap == 0,所以:
reserve(0);
什么也不做,紧接着:
data[sz++] = move(value);
即对 nullptr[0] 写。
所以最普通的:
vector<int> v;
v.push_back(1);
就有空指针写。
而你的 mergesort 恰好写了:
b.push_back({i, min(i+15,r)});
这是 rvalue,意味着 normal merge 路径第一次 push_back 就可能崩。
push_back(const T&) 还有一个典型 use-after-free:
v.push_back(v[0]);
如果这次需要扩容,value 引用的是旧数组元素;reserve() 先搬迁并 delete[] data,返回后 value 已经悬空,随后继续:
data[sz++] = value;
我用 vector<string> 实测,ASan 明确报 heap-use-after-free。
push_back(T&&) 对 std::move(v[0]) 同样存在这个别名问题。
另外,你这个 vector 并没有真正实现 vector 的对象生命周期:new T[cap] 会把整个 capacity 全部默认构造;pop_back() 和 clear() 只是修改 sz,不会析构被删除元素。资源要等整个数组重分配/析构才释放。reserve 又要求 T 默认构造 + move assignment,而不是正常 vector 的 placement construction。
at() 也完全不检查边界:
T& at(size_t index) { return data[index]; }
那它和 operator[] 没区别,名字却冒充有边界检查的 std::vector::at。
copy assignment 先 delete[] data 再申请新内存。如果 new 或元素赋值抛异常,对象已经被破坏,异常安全也不成立。
四、string::substr 是彻底写错了
这里形参也叫 len:
string substr(size_t pos, size_t len = (size_t)-1)
然后内部:
if (pos >= len) return string();
size_t actual_len = len - pos;
if (len < actual_len) actual_len = len;
你想访问的是成员 this->len,但实际全部访问了形参 len。
结果非常夸张:
s.substr(2, 2) 会因为 2 >= 2 直接返回空串。
s.substr(2) 的 len 默认是 ULLONG_MAX,于是计算出接近 2^64 的长度。我实测 string("abcdef").substr(2),ASan 报试图申请接近 0xffffffffffffffff 大小的内存。
正确逻辑应该围绕:
this->len - pos
和请求的 count 取最小值。
五、string += string 不支持最基本的自拼接
s += s 会进入:
memcpy(data + len, other.data, other.len + 1);
此时 other 就是自身,源区间和目标区间重叠,memcpy 对重叠内存是 UB。
我实测:
string s("abc");
s += s;
ASan 直接报 memcpy-param-overlap。
operator+=(const char*) 还有更危险的版本:
s += s.c_str() + 1
如果扩容,代码先释放原来的 data,传进来的 s 指针立即悬空,然后继续从这个悬空指针 memcpy,属于 UAF。
string 的 move constructor / move assignment 声明了 noexcept,但移动完成后居然给源对象:
new char[1]
这一步可能抛 bad_alloc。因为函数是 noexcept,一旦失败就是 std::terminate。这是非常不应该的 move 设计。
size() 返回 int 而成员长度是 size_t,字符串大于 INT_MAX 后长度会截断。find() 同样返回 int,大字符串位置也可能截断。
另外你允许 push_back('\0'),所以对象理论上可以包含 embedded NUL;但 find() 却使用 strstr,只能看到第一个 '\0' 以前的内容。这两个语义互相矛盾。
六、scanf 有内存破坏级 bug
你的 %d 是:
ll* val = va_arg(args, ll*);
意味着它要求调用者传 long long*。
但名字叫 %d,正常 C/C++ 使用者一定会写:
int x;
scanf("%d", &x);
你的代码会向 4 字节 int 写 8 字节。
我实测这一行,ASan 直接报 stack-buffer-overflow。
格式串里的普通字符/空格处理同样是错的。比如:
scanf("%d %d", &a, &b)
输入:
12 34
第一个 read_d() 已经读掉分隔空格;然后格式串走到那个 ' ' 时,又 _gc() 一次,于是把第二个数字的 '3' 吃掉。
实测结果:
12 4
而不是 12 34。
普通格式字符也根本没有验证是否匹配输入;基本只是“随便吞字符”。
%lld、%u、%i 等也没有正常实现;返回类型还是 void,无法得到成功转换个数或 EOF 状态。它不应该叫 scanf,因为语义和 scanf 差太远。
七、printf 的 varargs 类型读取有 UB
最明显的是:
case 'd':
out((ll)va_arg(args, ll));
真正的:
printf("%d", int_value)
传入的仍然是 int;你却用 va_arg(..., long long) 读取,类型不匹配就是 UB。
我实测:
int x = -1;
printf("%d", x);
输出:
4294967295
而不是 -1。
%.*f 的 precision 参数按照 C varargs 规则是 int,你却同样:
va_arg(args, ll)
还是 UB。负 precision 尤其危险,可能被读成极大的正 ll,然后进行几十亿次输出循环。
%lf 也坏了。我实测:
printf("%lf", 1.25)
输出类似:
140727147519704f
因为 %l 分支把 double 当 ll 从完全错误的参数类别里取,然后还把后面的 f 当普通字符输出。
%lld 同样解析不了。
浮点格式化也不正确:write_p() 是截断而不是四舍五入。例如 1.999 保留两位会得到 1.99,而正确格式化应该是 2.00。更荒唐的是 0.0 一开始就直接输出 "0",所以 printf("%.2f", 0.0) 实测也是:
0
而不是:
0.00。
NaN、inf 和超出 ll 范围的浮点数最终还会走浮点→整数转换,存在未定义/错误行为。
out(LLONG_MIN) 也确定有 UB:
x = -x;
LLONG_MIN 的正数无法用 long long 表示。我用 UBSan 验证到了 signed overflow,最终你的函数实测只打印:
-
八、EOF 的表示方式从根上有问题
_gc() 返回的是:
char
但 EOF 定义为:
-1。
这要求 char 恰好是 signed char。
一旦编译器使用 unsigned char,例如 -funsigned-char,-1 会变成 255:
c == EOF
永远不成立。
我用 -funsigned-char 编译后,让 read_s() 从空文件读,程序实测无限循环,直到 timeout。
反过来,在 signed-char 平台上,合法输入字节 0xFF 又会被误认成 EOF。
还有一个中文场景特别值得注意:UTF-8 字节通常大于 127,在 signed-char 平台会变成负数,而 read_s() / %s 用:
c <= 32
判断空白,于是大量 UTF-8 字节会被当成“空白/结束符”。这套 token 输入实际上不是 8-bit clean。
正确做法是 _gc() 返回 int,只有真实字节转换到 unsigned char 范围,EOF 独立使用 -1。
九、底层 read/write 也没有正确处理系统调用
这里:
ssize_t bytes_read = read(...);
_rend = _rbuf + bytes_read;
if (bytes_read <= 0) ...
如果 read() 返回 -1,你先计算 _rbuf - 1 指针,再检查错误。这个指针运算本身已经越出了数组允许范围。
而 read 返回 EINTR 等错误时也没有 retry,直接当 EOF。
输出侧 write() 的返回值完全被无视。POSIX write() 允许 short write;如果一次只写了部分数据,你直接:
_wptr = _wbuf
剩余部分就永久丢失。
输出也完全依靠最终 _return() flush。如果中途提前 return、异常退出等,缓冲区内容可能没写出去。
十、getline 对 CRLF 处理错误
你看到 '\r' 就结束,但没有把后面的 '\n' 一起消费。
输入:
abc\r\ndef\r\n
连续调用两次 getline,我实测得到:
abc|
第二行是空的,因为第二次调用首先读到了上一行留下的 \n。
十一、整数/浮点输入都缺少必要的数值正确性
read_d() 通过:
(x << 3) + (x << 1)
计算十进制累积,但 x 是 signed ll。超出 LLONG_MAX 后属于 signed overflow UB。
LLONG_MIN 也无法正常解析,因为你必须先累积出 9223372036854775808,这个值已经超过正 long long 最大值。
read_f() 同样先把整数部分放进 signed ll,大浮点数还没转 double 就可能先整数溢出。
它还不能正确读取 .5:会把小数点跳过去,最后把它解析成 5。
1e3 也不支持,会先返回 1,剩下的输入状态还会污染下一次读取。
十二、gcd 的 ctz 用错位宽了
你定义:
#define ctz(x) __builtin_ctz((x))
这是 unsigned int / 32 位版本。
但 gcd 接收:
ll a, ll b
然后直接:
ctz(a | b)
ctz(a)
ctz(b)
所以:
gcd(1LL << 40, 1LL << 41)
低 32 位全部为 0,于是调用 __builtin_ctz(0),这是 UB。
我用 UBSan 验证三个 ctz() 都报错,最终函数实测返回:
4294967296
即 2^32。
正确答案应该是:
1099511627776
即 2^40。
这里必须用 ctzll,而且还需要先规范处理符号。
十三、负数 gcd 会坏得更彻底
当前 binary GCD 根本没有先取绝对值,右移负 signed integer 本身就涉及实现定义语义,减法循环也不满足算法假设。
我实测:
gcd(12, -18)
程序直接卡住。
lcm() 又直接依赖这个 gcd,并且:
a / gcd(a,b) * b
仍然可能 signed overflow,也没有保证结果非负。
十四、你的 sqrt(x,n) 对非整数 n 数学上就是错的
代码里的 Newton 迭代用:
static_cast<int>(n)
决定幂次数,却又在公式里使用原始浮点 n。
于是:
sqrt(16, 2.5)
按函数语义应该求:
16^(1/2.5) ≈ 3.031...
你的函数实测得到:
4.0000000000000000
此外:
sqrt(x, 0) 直接返回 0
数学上并没有这种定义。
sqrt(0, negative_n) 也直接返回 0,同样不正确。
负 n、非整数 n、NaN/inf 都没有合理定义或验证,迭代也没有最大次数保护。绝对误差固定 1e-15 对极大/极小数也不是可靠的停止条件。
十五、IO 对常见整数类型甚至会编译失败
out 有:
int
unsigned int
ll
ull
但在 LP64 Linux 上:
long
unsigned long
是和这些不同的类型。
我测试:
long x; cout << x;
编译器直接报:
call of overloaded 'out(const long int&)' is ambiguous
unsigned long 同样 ambiguous。
输入侧也没有 operator>>(long&) / operator>>(unsigned long&)。
另外:
cin >> char
直接 _gc(),不会像默认 std::cin >> char 那样跳过 whitespace。这个接口长得像 iostream,语义却不一致。
十六、你实际上不能合法地“自己实现 std”
从第 99 行开始直接:
namespace std { ... }
然后自己定义:
std::move
std::forward
std::pair
std::vector
std::string
C++ 不允许用户随意向 namespace std 添加这种声明。这本身就是非标准行为。
并且一旦以后包含真正的标准头,例如 <string>、<vector>、<type_traits>,马上出现各种 redefinition/conflicting declaration。
我实际把 <string> 接在这份代码后编译,立刻出现:
conflicting declaration std::string
redefinition std::remove_reference
等大量错误。
十七、你自己 typedef 的 size_t 也是错的
你写:
typedef unsigned long long size_t;
在当前常见 Linux x86-64 ABI 上,真正的 size_t 是 unsigned long,虽然两者都是 64 位,却是不同 C++ 类型。
所以一旦标准头定义真正 size_t,就是 conflicting typedef。
ssize_t 也同样不应该自己猜类型。
这也是为什么这套代码几乎不能和正常 C/C++ 头文件共存。
十八、宏污染非常严重
你把这些全部定义成全局宏:
memcpy
strlen
abs
sin
cos
ceil
floor
round
exp
log2
EOF
NAN
……
这会污染用户代码甚至破坏后续头文件。
例如写:
std::abs(x)
宏预处理仍然会看到 token abs,可能改成类似:
std::__builtin_llabs(x)
直接把合法代码搞坏。
而你的 abs(x) 无论输入是什么都硬转 __builtin_llabs,浮点 abs 等语义也完全错了。
十九、有几个 builtin 宏实际上在 GCC 上一用就炸
例如:
__builtin_unpredictable
__builtin_assume
__builtin_types_compatible_p
这份文件之所以能编译,只是因为宏没有展开。
我用当前 GCC 14 单独展开测试,__builtin_assume 和 __builtin_unpredictable 直接报“not declared”;types_compatible_p 这套 C++ 写法也编译失败。
换句话说:这里混进了不同编译器/语言环境的 builtin,但没有做 feature detection。
二十、第一行的“超级优化 pragma”实际上大量无效
这一长串:
#pragma GCC optimize(...)
里面大量选项写错了。
我直接用 GCC 14 编译,收到大量:
bad option ... to pragma 'optimize'
例如你写的某些 f... 选项在 pragma 里又被 GCC 解释成额外加 -f,最后变成不存在的 -ff...。
flto、fuse-linker-plugin 一类本来就不是这样靠函数 optimize pragma 控制的链接阶段优化。
结果不是“极限优化”,而是满屏 warning + 很多选项根本没生效。
同时你还同时写了:
omit-frame-pointer
和类似:
fno-omit-frame-pointer
意图本身都是互相打架的。
二十一、强制 AVX-512 是一个部署炸弹
你写死:
#pragma GCC target("avx512f", "avx512vl", ... "avx2", ...)
这不是“CPU 支持就自动用”,而是在告诉编译器:
你可以假设运行机器支持这些指令。
如果编译服务器有能力生成 AVX-512,而最终 judge/目标 CPU 不支持,程序可能直接:
Illegal instruction
这在需要跨机器提交的竞赛模板里尤其危险。
tune=native 又进一步把生成策略绑死在编译机器上。
二十二、Ofast 和你自己的数学代码目标也冲突
Ofast 会打开 aggressive fast-math 一类优化,不再严格遵循 IEEE 浮点语义。
与此同时你的代码又显式构造 NAN、处理各种浮点边界。
如果你需要 NaN、signed zero、精确异常行为,Ofast 本身就不是无脑安全的开关。
二十三、临时栈数组让所谓泛型算法很危险
比如:
T stk[4096];
T temp_stack[1024];
如果 T 是 1KB 的结构体,第一个数组就直接吃 4MB 栈。
如果 T 是你的 string,每个默认构造还会自己做一次 heap allocation。
这不只是慢,而是非常容易栈爆、内存分配爆炸。
二十四、forward、pair 等只是“长得像 STL”,契约并不成立
你的 forward 第二个 overload 缺少标准实现用于阻止错误 forwarding 的约束;make_pair 也没有标准的 decay/reference-wrapper 语义。
string 连基本的 operator< / operator== 都没有,所以:
sort(string_array, ...)
使用默认 less<string> 根本编译不了,因为:
return a < b;
而你的 string 没有 <。
所以最好不要把这些类放进 std 并伪装成标准组件;它们的行为契约和真正 STL 差距太大。
二十五、最后,当前 main 实际什么也没做
现在最终程序只是:
prefetch(_rbuf, ...)
prefetch(_wbuf, ...)
_return();
没有读取,没有计算,没有输出。
如果这只是你的模板骨架没问题;如果这是完整提交代码,那当前程序当然没有任何业务结果。
修复优先级
这份代码不适合“哪里错补哪里”。核心模块已经互相叠加出太多 UB。建议顺序是:
第一优先级直接重写 five_way_partition / introsort_logic / mergesort。 当前 sort 已经有“整数排错 + 越界 + 移动对象丢数据”三个独立致命问题。
第二优先级修 pair / vector / string。 尤其是 pair::second 类型、vector rvalue 首次 push、别名 UAF、substr、自拼接。
第三优先级重新设计 IO。 _gc() 必须返回 int;printf/scanf 如果不能严格实现 C 语义,就不要使用这两个名字。LLONG_MIN、varargs 类型、short write、EINTR 都需要处理。
第四优先级删掉第一行那串 pragma 和强制 AVX-512。 在证明某项优化确实改善 benchmark 之前,这些东西目前带来的风险远高于收益。
所以评价非常明确:目前这不是一个“极致优化的 C++ 模板”,而是一个大量重新实现标准组件、但核心 correctness 尚未建立的实验性模板。性能优化做得太早,而正确性基础还没站稳。 最危险的不是慢,而是很多地方“正常数据也会错”,并且因为 UB 和 Ofast 的存在,错误还可能随编译器、CPU、优化级别改变。
全部评论 3
还在蒸,真有必要修改吗,这玩意 OIer 根本用不到
1周前 来自 浙江
0那对方都at我了,我还能不搞吗?况且对面明确要去GPT-5.6 Sol查找问题,我也不需要手动查找,也不浪费时间。
1周前 来自 浙江
0完全可以不理他
1周前 来自 浙江
1
1周前 来自 浙江
0干什么
1周前 来自 浙江
0这不是你让我帮你的吗?
1周前 来自 浙江
0为何要@我两次?
1周前 来自 浙江
0
1周前 来自 浙江
0





















有帮助,赞一个