Linus Torvalds 在 TED 演讲上所说的有品味的代码

2022 年 5 月 19 日
 Biwood

需求是从单向链表中删除一个指定节点。

教科书上的(普通的)写法:

void remove_cs101(list *l, list_item *target)
{
        list_item *cur = l->head, *prev = NULL;
        while (cur != target) {
                prev = cur;
                cur = cur->next;
        }
        if (prev)
                prev->next = cur->next;
        else
                l->head = cur->next;
}

优雅的(有品味的)写法:

void remove_elegant(list *l, list_item *target)
{
        list_item **p = &l->head;
        while (*p != target)
                p = &(*p)->next;
        *p = target->next;
}

目测充分利用的指针的特性,代码量少了不少。

代码仓库和详细解释在这里: https://github.com/mkirchner/linked-list-good-taste/blob/main/README.md

13768 次点击
所在节点    程序员
120 条回复
YuuuuuuH
2022 年 5 月 22 日
@enchilada2020 你说的 Stefan 是哪位呢,能否给个链接?谢谢
KMpAn8Obw1QhPoEP
2022 年 5 月 22 日
FrankHB
2022 年 5 月 23 日
懒得看视频,不过看原文描述,这个 taste 就很烂。
(这很正常,从 Linus Torvalds 这些年来干的活看本质是个 PM ,从不用指望 coding 水平多高就是了。)

对合格的 C coder 来讲,应该能看出,这里的烂首先不是代码上的,而是功能描述上的,其次是 API 设计;实现是之后的问题。
具体问题首先在于,就常识而言,remove 在这类上下文中说的是从一个数据结构中移除一个元素,同时 [维持资源不泄露这个最低要求的不变量] 的操作。
这里的 API 没附加说明,就说明违反了这个通用常识,这就是 teste 最明显烂的地方。

就这个 API 实际上做的事来 [逆向] 出要求,那么更应确信名字就是错的:这里就应该叫 detach_node 而不是 remove 什么东西。如果真是 remove ,原则上这里的 API 就还应该负责节点的释放。但是这里是上下文不足的——谁知道是要用 free 还是什么分配器还是 no-op ( intrusive list )?
不管是没有纠正这个低级设计缺陷去瞎改实现,还是要读者察觉味道不对、瞎猜需要解决的是什么问题(不过这点也可能是因为我懒得看视频里的上下文关系),整个过程在方法论意义上都是从头就 low 完了的,哪来的 taste ?

退一步讲,如果确信不需要释放资源,那么把名字改对以后,这个 API 的签名仍然很 low 。
注意,不要试图狡辩用户 [应该] 知道 target 对应的实际参数就应该能自己能处理资源——这是 C 用户中流行的、常见的、自作聪明的、典型的烂 taste 。
这种情况下,初始化 target 的实参调用前是 non-owning 的,结果调用后就 owning 了。如果没有显式说明,这属于影响 use sites 的重大接口信息遗漏。这种类型系统上检查不到的小把戏直接就能让任何负责理解清楚你要做什么的读者血压升高。
尤其愚蠢的是,这无谓地破坏了资源所有权的不变量。如果初始化 target 的实参是个(右)值,那么直接就漏了。对敢于写这种代码的用户来讲,众所周知,C 完全允许这样做且不经过复杂的指针分析,别指望静态能够发现问题。
借用 C 艹写法,真正能描述这坨逻辑干了什么的 target 实参类型至少应该是 variant<unique_ptr<list_item>, observe_ptr<list_item>>,一目了然地废话多;用 PL 话术来讲,这是滥用 ad-hoc polymorphism 。和滥用出参类似但远远更欠揍的那种。
只不过不幸的是,C 在表达清楚这些问题上的能力非常捉急(以至于我还得借用 C 艹这种半吊子),C 用户又特别喜欢拿因为表达力不足而被迫擦除类型了的中间实现来代替本来应有的正确 API 设计,这种烂 taste 导致垃圾 API 浑水摸鱼地混入了产品代码里。
而要实现“正确”的 detach ,结果一般就应该是返回值。

……于是,C 水平不足的用户的实现上的小聪明,还用得着多婊么?
如果看不出来问题或者死活没思路怎么改对,说明就没怎么会 C (毕竟怎么避免这种基本的坑是会用 C 的刚需),建议重新学习 C 。还是不行,那大概率说明你不配享受 C 的所谓信(偷)任(懒)程(没)序(特)员(性)的特色,换个能强迫你把设计搞对路的语言,回炉重造。
FrankHB
2022 年 5 月 23 日
@icyalala C 真不适合什么 performance freak 。反正一要可移植性,freak 起来就等着暴死好了。
其实一般用户在这里能做的撑死也就是看看汇编慢慢试罢了,远远不到 freak 的程度。(见过被 AT&T 汇编语法搞烦了写 shellcode 的,这也算不上多 freak 。真 freak 就去自己糊 code generator 去了,比如 Chez 那种故意踹开汇编器的。)
我日常大抵也算不上;然鹅殊不知我无中生有插几个[&] () __attribute__((flatten)) {}(当然,C 这里到现在还是得依赖更多的 GNU 扩展等魔法)就极有可能随随便便爆杀你死活调半天甚至自己糊__asm__的代码。
当然,对我来说,典型下场是(因为能优化的解空间太大剪枝困难)编译时间比改代码时间多若干倍(虽然我也可以在此期间 review paper 或者补番),以及最近升级 GCC 12 生成烂了的代码还得傻乎乎 bisect 多加__attribute__((optimize("no-tree-pta")))(要么全局-fdisable-tree-einline ,代价太大)之类拖延工时的 hack 。不过没 ICE 就谢天谢地了。

(另外一个感想也就是 CS 101 还在教这种东西啊……这哪 CS 了。看来大部分网红课跟以前的 MIT 6.001 这样的经典的差距真不只是一个层次的。)
FrankHB
2022 年 5 月 23 日
……上面一堆 typo ,先不改了。
@cnbatch ({}) 这 expression statement 这哪里不是 GNU 扩展了(亏上面我还想故意省略掉无关紧要的话题……)……

其实这里的扩展,不管是({})还是__typeof ,直接换 C 艹就都不用扩展了。但是 C 艹就不兴这套。
说白了,container_of 和 offsetof 这种直接对语言提供了 static layout 的 ABI 限制的特性,与其说是信任程序员,还不如说是嘲笑程序员对可移植性需求的坚持没有 13 数。(既然都能假定 layout 全静态确定了,struct 咋就没个像样的通用写法,还得 bitseg 和-mxxx 敲捣呢?)
当然,不是说没有就好哪去。C 艹的 non-standard layout 抓瞎的问题更加说明了半吊子(依赖 non-standard layout 随随便便 UB ,实现又都不愿意扔掉 ABI compat )。但这和滥用 layout 的假设是两回事。

其实一看有人拿 kernel 里的#define 和__typeof 当例子,我就会想起来 C 艹好歹没这种程度的问题:
lwn.net/Articles/750306
什么相信程序员,明明就是语言设计无(废)能(物)罢了……

另外 FreeBSD 把这种功能的代码放这个位置,说明全局工程质量上挺烂的……(虽然局部往往不如 Linux 辣眼睛。)
FrankHB
2022 年 5 月 23 日
淦,上面几楼迷惑 typo 太多,自己都读着不顺,一起注释得了……
“初始化 target 的实参是个(右)值”//特指没 aliasing 的,比如直接新分配的。
“observe_ptr”→“observer_ptr”
“滥用 ad-hoc polymorphism”//指 C 的 list_item*和精神 unique_ptr<list_item>/observer_ptr<list_item>之间实质相当于存在一个莫须有的 coerce (其它的真实的 coerce 也有,比如 malloc 结果直接初始化对象指针)。而 C++实际当然也不允许这种橄榄 type safety 的屑操作。
“target 实参类型”→“target 形参类型”
“被 AT&T 汇编语法搞烦了写 shellcode ” //主要大约因为是 clang 看来不兹瓷 GAS 的.intel_syntax 。
“更多的 GNU 扩展等魔法” //主要就是 statement expression ,或者^block 之类(没试过)。
“expression statement”→ “statement expression”
“ bitseg”→“bit field”//刚好看某些 patent 上头手滑了……
lwn.net/Articles/750306”→“lwn.net/Articles/749064”//其实前者也通,但得 C++20 。

* * *

@ColorfulBoar #29
你这 list_head 扫一眼就能让人盲猜是国内流行的数据结构书的流毒……

@GeruzoniAnsasu 节点(node)!=结点(knot),谢谢。
所谓的不好写其实是逻辑混乱的结果。
因为原则上指向节点的指针就只是个表示节点的指针,整个链表本来就不是节点的同义词,原则上继续使用节点指针指代就是个不应当被依赖而容易引起理解混乱的实现细节,至少也应该用类型别名屏蔽掉。
(不过也有上面那个渡劫失败搞得正常的 list_node*也被混着当成了 list_head 的反面教材了。)
但 typedef/using 其实是治标不治本,因为不 oqaque ,不够提醒手贱的后果,多少有些自欺欺人……还不如直接套个 struct List{list_node* p;};,不要什么 head ,那就真是不同的类型了(而且还能借 TBAA 帮助优化,真乱转会被 strict aliasing 寄掉),在所有权上也更清晰(有效避免 Rust n00b 想不清楚链表怎么糊综合征)。实际上现在像样的实现也都是这么干的,并没什么“教科书写法”不同的争议,看所谓的数据结构书反而是被坑了,还不如直接扒拉实用的源码(暴论)。
设计上还有值的注意的一点,就是大多数需要链表的情形其实仅仅是需要抽象数据结构的一些渐进特性(比如 O(1) splice ;题外话,如果没 splice 或者防止 invalidation ,基本就不该用什么链表),因此不要求知道真正意义上的端节点,所以可以实现成环形列表(如 libstdc++的 std::list ),这样这里就更不需要纠结 null 怎么处理了。(代价是 list 不能 trivially relocatable ,不过保守估计 99%以上的 C/C++用户从来都不会想到这个问题,反正被坑的优化空间本来也用不上,C++26 (?)前八成不用纠结。)
cnbatch
2022 年 5 月 23 日
@FrankHB 我后来也想起({})也是扩展,显然一旦长时间习惯 GNU 扩展的坏处,就是连眼睛看见后都忘了“这是扩展”。这确实不是什么好事。

另外关于 FreeBSD 的 container_of ,实际上 FreeBSD 自己并不用这种东西,它里面使用到 container_of 的只有极个别驱动(其中一些或许是从 Linux 版本移植过来的,例如 nvidia tegra 的驱动,当然了我没同步对比过,所以只能讲“或许”),以及 Linux 相关的东西(比如 Linux 兼容层、OpenZFS 跟 Linux 共用的部分文件)。

由于 FreeBSD 自己不用 container_of ,因此它也没必要专门弄一个 container_of 文件。但既然外来的代码要用,那就只能局部使用,不能让 container_of“污染”整个 FreeBSD 内核树。

其实不难理解他们为什么不喜欢用 GNU 的扩展,FreeBSD 早在十年前就把默认编译器从 GCC 换成了 Clang ,这就表明了态度。
ColorfulBoar
2022 年 5 月 23 日
@FrankHB 然而我还真没看过国内流行的数据结构书(和受它影响写出来的任何东西)……我写那坨的唯一目的是说明它本可以写成不用让观众人肉解析几层指针的样子,其他和这个目的无关的地方都是能省就省(比如直接 using 而不是包一层 struct )。至于为什么叫 list_head 可能是我当时在想 list 可以递归定义成 list = empty | (list, data),所以就写成这样玩了(没直接叫 list 估计是当时想着这个是不 export 给用户的内部实现),我完全同意主楼那个代码已经是坨烂摊子了正经写肯定得从头开始重新设计……所以有啥类型系统和接口定义得比较讲究的链表实现适合参考么?
我刚写完的时候自己看着中间那个强转也恶心,总觉得有 strict aliasing 的问题,但后来感觉标准应该保证了 standard layout 的情况下指向第一个 non-static data member 的指针和指向整个 struct 的指针可以互转 + 我一直在用同样的类型来访问,看起来是不会出 UB 的(还是我漏了啥?)。另外我感觉那帮在 rust 里死活写不出来一个链表的是因为非得禁用 unsafe……
关于 ownership……为什么 target 调用后是 owning ?又不是 std::forward_list::erase_after (其实我也没太搞懂为什么 erase_after 传了个 const iterator 进去,好像 ownership 上也不对劲),这个里面的 target 只参与了比较(所以才很奇怪地在大概率已经在前面找到过前一个 node 了现在还得从头再搜一遍,我觉得性能上这个才是最烂的地方),一直都是 non-owning 的吧(然后我想起来我那坨代码最明显的问题是 target 的类型不对,应该换成一个明确显示是 non-owning 的)
zinwalin
2022 年 5 月 23 日
谢谢分享
zinwalin
2022 年 5 月 23 日
优雅不一定易读,但程序员知道有更优雅的写法,其实好处很多,在自己的个人项目里就可以用,记得加注释。
pastor
2022 年 5 月 23 日
@FrankHB #83

这么多年过去了,c/c++领域论杠精和嘴强王者我绝对把你排第一,但陷入细节太深本身也是中毒。很多观点你都像是在虚空论道、口若悬河、不食人间烟火——不食人间烟火是因为你更多偏于理论、脱离工程。c++本身的复杂本身就由于需要耗费工程师大把内功修炼时间而导致脱离工程实际,否则何苦又有人去搞 rust 。
在 c++领域的很多讨论,你都没有输,你是上帝一样的存在。但就像你的昵称一样,前面带了个“幻”字,你所自我感觉良好的那些东西,很多都是假象。
这样评论,有人可能会觉得我只会无脑输出,但看到那句“从 Linus Torvalds 这些年来干的活看本质是个 PM ,从不用指望 coding 水平多高就是了”是实在忍不住了。
幻上帝至少十几年前就已经很出名了,我也早就知道。我也有自知之明,c++语言本身的语法语义各种知识,我是比幻上帝甚至这里的一些人都要差很多的,因为我更在乎工程实际,够用好用即可,减少工程复杂度、提高工程效率才是我所追求的。
我无意去吹捧社区领袖,因为 linux 和 linus 的贡献、地位都是显而易见、毋庸置疑的。
但区区一众凡人,非要踩领袖来抬高自己,从来都不觉得小丑可能是自己吗?
pastor
2022 年 5 月 23 日
@ColorfulBoar #88 难道你们在说出 linus 没有 taste 的时候都不思考下自己的代码是否就真的有 taste 吗?非要踩行业神仙来抬高自己,上一楼回复幻的话同样送给你,你的代码我前面也评论过了
ColorfulBoar
2022 年 5 月 23 日
@pastor 难道你在舔 linus 之前不反思下自己连引用都整不明白有没有资格吗?
pastor
2022 年 5 月 23 日
@ColorfulBoar

哦,你这个是左值引用还是什么是吧?那是我 2 3 说错,我对 c++11 及以后没什么研究,我无知了

但问题又来了,c++这各种语法语义,比如就我犯错的这个地方就很容易跟指针弄混,要想不弄混得先来个内家十年不出门的修炼,就真的漂亮了?

另外我说的 1 4 ,就不是问题了?

再者,逻辑清晰一点,你也不要随便给别人乱扣帽子说舔,我没有舔,我只是对行业领袖心存敬畏尊重
也建议你不要说资格,啥时候尊重别人还需要先自己也要在那个领域很牛逼才有资格了?难道我尊重爱因斯坦首先我也得是个物理学大牛?

张口就来踩别人行业领袖烘托自己牛逼很高尚?而且如果真要讲资格,那我也想问问你,阁下有哪些成就配得上让您居高临下踩 linus ?别说成就,就 list_head 相比与 list_item 的 taste 就更好是吗?

你们深度中毒的 cpper 可真是天下无敌,连被喷邪教的 gopher 见了你们都得礼让三百分

我只是看不惯你们张口就来说别人行业领袖不行,乱带节奏,但我可没乱给你扣帽子舔或者怎么样,所以也请你们逻辑不要那么混乱地乱扣帽子
浮躁的时候谁都有,遇到机会了扪心自问下改正了提高下自己的修养可能更香
程序员多数都懂着一些 linux 相关的呢,送人玫瑰手有余香,赞赏肯定一下别人有那么难吗
pastor
2022 年 5 月 23 日
@ColorfulBoar 而且,既然 cpper 可以跨界去喷 linus 的 c ,我这种 c++11 前的人评价你 c++11 后的的 c++也相当于跨界了,本质也没什么不同,别扯什么资格不资格的
ColorfulBoar
2022 年 5 月 23 日
@pastor 所以你真的是啥都不懂……这跟左值右值有什么关系?用引用传参代替指针传参不是 C++98 里面就有的东西?第一天学引用的人都能花 10 秒的时间把它改成用指针的版本,这种东西要花十年……你是不是以为引用的背后有个什么庞大的 runtime 在支持它所以只要用了引用就不可能改写成能进内核的版本了?
pastor
2022 年 5 月 23 日
@ColorfulBoar 那咱先不要喷 linus 的话题了,改成友好的技术交流

这里的
auto p = list_head(&head);
因为 head 作为函数参数是引用进来的,所以这里的 head 是 list_node*对吧?
然后,list_head((&head)) 这里在 list_head() 应该是类型转换吧,如果不考虑 list_head() 转换后,只考虑 (&head) 它是转成左值引用还是取地址变成 list_node**?如果是变成 list_node** 这个属于 11 之前,我是知道大概的,但是 11 之后的我确实不懂,虚心请教
cnbatch
2022 年 5 月 23 日
auto p = list_head(&head);
右边的 & 符号是取址,一直以来都没变化,到了 C++20 依然还是取址(不考虑运算符重载)

也没有什么“转换成左值引用”这种说法吧,只听过“转换成右值引用”,没见谁说过“转换成左值引用”。你提到的“左值引用”的是指 int &re = other 这种吧,这种就是 C++98 以来的常规引用,不需要也没人会讲“转换成左值引用”。

区分很简单,单个 & 的语义从 C++98 以来都没变化。两个 & 的时候才是跟右值引用有关。左值引用跟右值引用的区分直接看 & 的数量就行。
icyalala
2022 年 5 月 23 日
@FrankHB
"从 Linus Torvalds 这些年来干的活看本质是个 PM ,从不用指望 coding 水平多高就是了。"
pastor
2022 年 5 月 23 日
@ColorfulBoar 很久没写 cpp ,我调试了下,请看注释的部分吧:

#include <stdio.h>

struct list_node;
using list_head = list_node *;

struct list_node
{
list_head next;
int value;
};

void remove(list_head &head, list_head target)
{
// 这里确实是二级指针,其实内存已经乱掉了;
// 但因为 list_node 的第一个字段是 next 指针,而你的 p 的值是 head 的值,也就是说,p作为 list_node 的第一个字段的值刚好跟 head 是内存是同一段内存
auto p = list_head(&head);
// 所以这里的 while 循环的第一次判断 (p-> != target) 实际上是判断 head != target ,所以整个循环下来,也能得到正确的结果
// 但是这依赖于 list_node 的字段的内存布局,list_node 的 next 字段必须是结构体第一个字段才能正确运行
// 否则,把 value 放在 next 前面,或者在前后中间再多加点字段,野指针可能就 coredump 了,或者运气好由于出入栈之类的刚好指向了哪个 ok 的地址没 coredump 但是也没有删除成功,我已经试过了,不信你改下代码试试
// 当然你也可以把 head p 以及他们作为指针只想的 list_node 各个字段的值都打印出来,然后你就发现了,p->next == head ,这只是内存布局巧合导致你运气好而已

// 单纯讨论 cpp ,正确的写法应该是 auto p = head;

int cnt = 0;
while (p->next != target)
{
p = p->next;
cnt++;
}
p->next = target->next;
printf("cnt: %d\n", cnt);
}

void travel(list_head head)
{
printf("----------------------\n");
auto p = head;
while (p)
{
printf("value: %d, %p, %p\n", p->value, p, p->next);
p = p->next;
}
printf("----------------------\n");
}

int main()
{
list_node node1, node2, node3;
node1.value = 1;
node2.value = 2;
node3.value = 3;
node1.next = &node2;
node2.next = &node3;
node3.next = nullptr;

list_head head = &node1;
list_head target = &node2;

travel(head);
remove(head, target);
travel(head);
}

@cnbatch #98 所以按你说的,我 #46 最初就没说错对吧?我是被他第一次说我不够资格还以为是什么 11 之后的高级东西我确实不懂呢,也看下我上面调试的吧,如果我说的没问题,那你们之前就没人仔细看下他这个代码啊?。。。:joy:

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://study.congcong.us/t/854016

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX