V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Biwood
V2EX  ›  程序员

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

  •  2
     
  •   Biwood ·
    oodzchen · 2022-05-19 22:02:14 +08:00 · 11949 次点击
    这是一个创建于 917 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    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

    120 条回复    2022-05-25 01:50:41 +08:00
    1  2  
    pengtdyd
        1
    pengtdyd  
       2022-05-19 22:04:52 +08:00
    代码是优化出来的,一开始写,没经验的人是很难写出优雅的代码的
    zhengxiaowai
        2
    zhengxiaowai  
       2022-05-19 22:06:17 +08:00   ❤️ 26
    不好,如果性能没有明显差异,第一种比第二种好一百倍

    至少我能一眼看出来第一种在做什么
    k9982874
        3
    k9982874  
       2022-05-19 22:08:16 +08:00 via Android
    看项目组成员水平吧,linux 内核开发组都是大佬第二种没问题,放到小厂就是装逼
    pca7a2
        4
    pca7a2  
       2022-05-19 22:08:17 +08:00   ❤️ 3
    能让更多的人更容易看懂的代码才是好代码!
    levelworm
        5
    levelworm  
       2022-05-19 22:13:21 +08:00 via Android
    大佬写代码都几十年了,整天和指针什么打交道。
    cmdOptionKana
        6
    cmdOptionKana  
       2022-05-19 22:18:55 +08:00   ❤️ 3
    从 geeker 的角度看,肯定是追求 Linus 那种代码。从企业螺丝钉的角度看,那就是简单直白,新人容易接手的代码好。
    codefever
        7
    codefever  
       2022-05-19 22:23:59 +08:00
    好雨知时节,当春乃发生
    Jooooooooo
        8
    Jooooooooo  
       2022-05-19 22:27:01 +08:00   ❤️ 2
    "目测充分利用的指针的特性,代码量少了不少。"

    如果这是"优雅的", 那"优雅的"并不好.
    leimao
        9
    leimao  
       2022-05-19 22:28:10 +08:00 via iPhone
    妙啊妙啊
    leimao
        10
    leimao  
       2022-05-19 22:29:40 +08:00 via iPhone
    我自己写代码的时候,喜欢把代码优化过程都 comment 保留下来。但是工作上被人要求只保留一份,其余全部删除。
    leimao
        11
    leimao  
       2022-05-19 22:30:49 +08:00 via iPhone
    话说工作好多年了,从来没在工作中有机会写 linked list
    loading
        12
    loading  
       2022-05-19 22:31:46 +08:00   ❤️ 1
    谁给老子写优雅那种我就炒掉谁,我们这写的不是底层代码。
    Danswerme
        13
    Danswerme  
       2022-05-19 22:53:57 +08:00
    对于普通的程序员来说写这么有“品味”的代码是在折磨自己和队友,大佬当然无所谓。
    xfriday
        14
    xfriday  
       2022-05-19 23:01:37 +08:00   ❤️ 12
    @zhengxiaowai 第二种可读性比第一种好多了,真不知道你的 “第一种比第二种好一百倍” 结论哪里来的
    xfriday
        15
    xfriday  
       2022-05-19 23:02:51 +08:00
    @loading 你是老板?我看你在大厂里只有被优化的份
    masterclock
        16
    masterclock  
       2022-05-19 23:08:08 +08:00   ❤️ 2
    优雅啥的不管,即使是单句的代码块,我也喜欢加 {}
    但,看、写内核代码,都是没有 {} 的,难受啊
    enchilada2020
        17
    enchilada2020  
       2022-05-19 23:14:56 +08:00 via Android   ❤️ 3
    吵这种事情没有意义 对语言的掌握程度不同 对代码可读性与优雅的理解也不同 对于大部份人来说难以阅读不好理解的写法 可能对于某些特别擅长该语言的使用者来说是常规操作

    不用说 Linus 这种级别的怪物 不理解的可以可以去 LC 上看看 Stefan 的代码 他的所有题解中所有语言的写法都非常巧妙
    L4Linux
        18
    L4Linux  
       2022-05-19 23:15:05 +08:00 via Android
    说这是利用指针特性其实不准确。这其实是对重复的过程,在这里是修改某一指针的指向,进行一般化。

    一般来说应该鼓励这么写。
    adoal
        19
    adoal  
       2022-05-19 23:20:06 +08:00   ❤️ 28
    Linus 这种写法的妙处不在于代码量少了多少。行数少不一定是好代码。关键在于,通过使用指针的指针,把需要修改->head 和 prev->next 的情况统一起来,不需要给边界情况 l->head 单独写一个分支来修改,把特殊情况化为一般情况,一致性好,实际上代码(因为边界情况考虑不周而)出错的概率更小了。
    PMR
        20
    PMR  
       2022-05-19 23:25:43 +08:00
    各有各有点 协同开发要看接头人的能力

    在寸土寸金的编程 垦定要上第二种
    adoal
        21
    adoal  
       2022-05-19 23:26:32 +08:00   ❤️ 1
    话说如果我来写的话,第一反应肯定想不到 Linus 的写法,还是会对边界做判断的。但我会把判断写成卫式,在循环之前,当 target 就是 l->head 时替掉 l->head ,而不是循环结束才根据 prev 是否为 NULL 来判断。那样我觉得不如先判断更符合直觉。
    Leviathann
        22
    Leviathann  
       2022-05-19 23:29:34 +08:00
    意思就是说 C 可以拿到 指向关系 这个东西本身并修改是吧
    LeegoYih
        23
    LeegoYih  
       2022-05-19 23:35:47 +08:00
    确实牛
    kidlj
        24
    kidlj  
       2022-05-20 01:01:44 +08:00
    爱丽跟头!
    GeruzoniAnsasu
        25
    GeruzoniAnsasu  
       2022-05-20 03:13:33 +08:00   ❤️ 1
    看了这个帖子感觉很多人的代码修炼筑基并不那么理想


    「学术地、迂腐地」来讲,教科书上链表有两种典型实现,一种是用空的头结点来作为表头,一种是用指向第一个结点的指针来作为表头。


    如果用头结点来实现,那天生可以与第二种「同样简洁」:

    list_item*p = &l->head // 注意 head 是元素了,不再需要多一个 prev 了
    while (p->next != target) p=p->next // 注意 p 不可能 null ,p->next 一定是存在的( p->next 可以 null 但->访问必定合法)
    p->next = target->next // 除了 target 为 null 这种原代码也不考虑的边界,其它条件都不需要 sentinel


    linus 给出的「优雅版」本质上是,把指针本身视为节点,p 指向的是元素结构的 [next 指针],而使用「头指针」实现的链表虽然第一个元素可能不存在,但第一个「 next 指针」是必定存在的,这跟我上面给的头结点例子写法就基本一样了

    不过为什么基本见不到使用头结点实现的链表呢,因为 1. 浪费无谓的空间 2.泛型化就不好写或不优雅了
    rpman
        26
    rpman  
       2022-05-20 04:22:53 +08:00   ❤️ 4
    两个 case 都没有考虑 target 不在链内的异常
    mingl0280
        27
    mingl0280  
       2022-05-20 04:45:48 +08:00 via Android
    ……这还要考虑第一个有蛋用,C/C++的单向链表不都应该默认写成第二种形式?说得那么玄乎,不就是拿二级指针替掉了 prev ?你把 list_item 视作`template<Typename T>`就是天然的泛型化了。
    这还看不懂还要解释水平有多菜?
    yzbythesea
        28
    yzbythesea  
       2022-05-20 05:03:11 +08:00   ❤️ 3
    The most interesting port to me is that last if statement (1st approach), (2nd approach) is better and it does not have the if statement. It rewrite it so that a *special case* goes away and becomes the normal case. And that's good code.

    I sent your this stupid example, that is not relevant because it is too small. Good taste is much bigger. Good taste is about really seeing the big patterns and kind of instinctively knowing what's the right way to do things.

    Linus 的原意是第二种写法去掉了对于 edge case 的处理,属于 generalize 了代码,看到了 big pattern 。并不是要秀什么语法或者算法技巧。
    ColorfulBoar
        29
    ColorfulBoar  
       2022-05-20 05:42:33 +08:00   ❤️ 6
    我是没看出来 Linus 有啥 taste……正常人会写成这样,扫一眼就知道在干什么

    (你说里面是不是有 UB ?反正出事也是 container_of 这种玩意比我先死)
    weyou
        30
    weyou  
       2022-05-20 08:36:47 +08:00 via Android
    不用考虑节点找不到的情况么?
    weyou
        31
    weyou  
       2022-05-20 08:42:28 +08:00 via Android
    @ColorfulBoar 确实,这个才是一般能想到的写法
    Austin2035
        32
    Austin2035  
       2022-05-20 08:44:34 +08:00
    不做评论,分享一下 Redis 双向链表的删除节点写法

    /* 删除一个节点 */
    void list_del_node(list *list, list_node *node)
    {
    /* 判断该节点是否有直接前驱 */
    if (node->prev)
    node->prev->next = node->next;
    else
    list->head = node->next;

    /* 判断该节点是否有直接后继 */
    if (node->next)
    node->next->prev = node->prev;
    else
    list->tail = node->prev;

    /* 释放该节点 */
    free(node);
    list->length--;
    }
    luxor
        33
    luxor  
       2022-05-20 08:57:39 +08:00   ❤️ 1
    源码上的优雅不代表编译后也是高效的。长得好看不等于就有饭吃。
    Biwood
        34
    Biwood  
    OP
       2022-05-20 09:44:38 +08:00 via iPhone
    @luxor 也许你们以前所说的“优雅”不是真正的优雅,这个例子里的优雅代码恰好是更高效的写法,你可以看看原文的解释。感觉就像是裁剪掉了不必要的细枝末节,回归事物原本的简单自然。
    greygoo
        35
    greygoo  
       2022-05-20 09:47:05 +08:00   ❤️ 1
    @luxor 优雅的写法的确生成了更高效的代码: https://godbolt.org/z/6qo3Yqez3
    hitmanx
        36
    hitmanx  
       2022-05-20 09:52:27 +08:00
    @adoal 我估计我第一反应也是如此。这种方法可读性也挺好的,除了还有分支以外。
    agagega
        37
    agagega  
       2022-05-20 10:31:10 +08:00
    明显第二种好理解,因为省去了不必要的分支条件。写 LeetCode 各种带 Corner Case 的题时就很想要这种代码
    bung
        38
    bung  
       2022-05-20 10:45:07 +08:00
    对指针很有感觉的人,看第二种好理解。

    很多人见到多级指针要延迟反应一下,就觉得第一种好理解。
    hccsoul
        39
    hccsoul  
       2022-05-20 10:50:07 +08:00
    你自己写自己的项目想怎么优雅怎么优雅 上班写业务代码可读性还是比较重要,你永远不知道你同事的水平有多差你写的再好他也看不懂
    qqg1530
        40
    qqg1530  
       2022-05-20 11:04:47 +08:00 via Android
    可以但没有必要
    lonewolfakela
        41
    lonewolfakela  
       2022-05-20 11:05:19 +08:00   ❤️ 1
    本质上和 OP 给的第二种写法是一回事,但是明显清楚多了……

    void remove_elegant(list& l, list_item& target)
    {
    for (auto& it = l.head; l != nullptr; it = *(it->next))
    {
    if (it == target)
    {
    it = target->next;
    break;
    }
    }
    }

    事实证明,没事不要写 C 语言,即使不用到各种 fancy 的工具,用 C++写出来的简单的 C with class & reference 代码也能好很多……
    lonewolfakela
        42
    lonewolfakela  
       2022-05-20 11:10:25 +08:00
    啊应该是 for (auto& it = *l.head; it.next != nullptr; it = *(it.next)) 不过这么搞就得额外判断最后一个元素了……
    emmmm 好吧果然引用和指针混在一起还是八字不合。从一开始就应该好好地包装成迭代器才对……
    ksco
        43
    ksco  
       2022-05-20 11:17:35 +08:00
    某些人真是又菜又傲慢。我觉得自己写不出来优雅的代码不可怕,但拒绝接受优雅代码固步自封就挺可怕的。
    rshun
        44
    rshun  
       2022-05-20 11:22:34 +08:00
    第一种写法和第二种都能实现,但 linus 提供另外一种思路,我觉得这才是最重要的。
    luxor
        45
    luxor  
       2022-05-20 11:37:19 +08:00
    @greygoo 你确认?当 jne .L11 不跳转的时候,要执行 3 条指令,而“不优雅只需要执行 2 条指令”。
    pastor
        46
    pastor  
       2022-05-20 12:55:14 +08:00
    首先声明:我水平很一般。

    正题:指针、二级指针在内核社区,或者在嵌入式领域,都是很常规的操作,而且主题部分的两段代码,第二段确实节省了语句指令、更简洁。应用层的 CV 程序员就不要乱说 linus 的不好了吧,首先提高下自己的水平再战好不好?又菜又不承认也就算了,还非要站出来去指责自己领域里神仙般的存在的代码,你们这些这样“我不要你觉得,我要我觉得。。。”的大神可真让人开眼。

    @ColorfulBoar #29
    1. linux 内核不支持 cpp ,所以请不要用 cpp 引用这种来对比内核代码了吧
    2. auto p = list_head(&head); 如果我没看错的话,head 自己是指针,&head 是二级指针,这里相当于把&head 这个二级指针强转成一级指针了吧?所以后面的操作内存应该已经是乱掉了吧?我很久没写 c/c++了,不知道是不是我看错了,各位确认下吧
    3. 这段代码,引用和指针都用,看上去比 linus 的版本更容易迷惑人,比如我 2 中提到的,然后还有人点赞。。。
    4. 单从命名上讲,list_head 这个也不合理啊,next 之后它还是 head 吗?明明是 list_item 更合理好不好?
    icyalala
        47
    icyalala  
       2022-05-20 13:04:28 +08:00   ❤️ 2
    来看看这些回复:
    "第一种比第二种更好,因为第二种我看不懂"
    "让更多人看懂的代码才是好代码"
    "写优雅代码的我会炒掉,我们不写底层代码"
    fnmain
        48
    fnmain  
       2022-05-20 13:30:47 +08:00
    站在不同人的角度,得出的结论可能是相反的,各方理由也都很有说服力。
    只从可读性分析:
    玩指针非常熟练的人,Linus 的方法更合适,因为少一个变量,占用头脑中寄存器的数量就少。
    但如果解指针带来的脑力负担大于多维护一个变量的负担,就更适合教科书上的方法。
    因此不同基础和编程习惯的人,对这个问题的看法会不一样。当遇到一个你不认可的观点,急于否定之前可以先思考一下对方这么想的原因,何况这个人还是 Linus ,或许值得你为此多花半分钟。
    mingl0280
        49
    mingl0280  
       2022-05-20 13:43:43 +08:00
    @icyalala 确实让更多人看懂的代码才是好代码,但是第二种根本不存在“看不懂”——看不懂的连本科生水平都当不到啊……是怎么混进程序员行业的?
    banmuyutian
        50
    banmuyutian  
       2022-05-20 13:46:21 +08:00
    虽然我是 CRUD BOY ,但觉得第二种好
    nevin47
        51
    nevin47  
       2022-05-20 14:32:23 +08:00
    @pastor #46 五年前我还没接触 Kernel 代码的时候我也会觉得第二种是异类

    后来无意间涉足了内核,然后真的摸爬滚打了好几年,慢慢也就习惯了这种思维方式了(更贴合内存与硬件的思维形式)

    其实这个是应用层开发与系统层开发的思维的区别,我们在公司内的一些研讨会上也会讨论类似的问题,“明明都是 C 语言,为什么底层软件的代码上层软件的工程师普遍觉得看不懂”。后来的结论就是思维方式不同导致了看不明白,就像是背完了英文单词,但是照样阅读理解做不及格。

    当然,人们面对未知领域……天生存在着一些傲慢,也是正常表现😹
    GopherDaily
        52
    GopherDaily  
       2022-05-20 14:34:58 +08:00
    如果团队的同学都能看得懂优雅的那种,我自然倾向于第二种。
    实际情况是,大多数同学的水平和我们的产出都适合第一种。
    crisrock
        53
    crisrock  
       2022-05-20 14:48:06 +08:00
    除了看不懂,第二种没毛病
    dingwen07
        54
    dingwen07  
       2022-05-20 14:50:11 +08:00
    其实第二种可读性也很高
    xxv0
        55
    xxv0  
       2022-05-20 15:06:08 +08:00
    我感觉很多人看不懂都是因为没有直接把指针当作一个值来理解,看到指针考虑的都是指针指向的值。把 p 这个二级指针当作一级指针来看的话,把*p 当作一个普通的值,会发现第二种省去了很多中间变量,第二种可读性更高。
    xxv0
        56
    xxv0  
       2022-05-20 15:08:41 +08:00
    就像是 @GeruzoniAnsasu 说的一样,把指针本身视为节点。
    cloudsigma2022
        57
    cloudsigma2022  
       2022-05-20 15:24:22 +08:00
    为什么我看第一种,半天不知道它在绕什么,第二种秒懂呢?
    pastor
        58
    pastor  
       2022-05-20 15:53:18 +08:00
    @nevin47 #51 其实我怀疑那帮说 linus 写法不好的小年轻是不是连 linus 是谁都不知道。见惯了 ctrl+cv 程序员评价语言、框架、写法这不好那不好的,但 cv 程序员自己基本功都没扎实呢,所以幸好有 linus 之类的独裁型社区领袖,否则各种乱建议说不定就把好玩意搞砸了
    whyso
        59
    whyso  
       2022-05-20 16:04:46 +08:00
    雅到极致不风流
    nevin47
        60
    nevin47  
       2022-05-20 16:18:18 +08:00
    @pastor #58 主要还是上层应用的编程思维和操作系统的编程思维的区别。

    就像上面有层在吐槽 container_of 一样,实际上 container_of 是公认的一个精妙绝伦的设计,而且这个宏在 compile time 也有保护的。如果不是对编译原理、内存管理、C 语言基础极其精通,是不可能设计出这个宏的。如果一直带着上层应用:我们要简化代码、要防止任何异常输入,这种思维来看内核,那永远体会不到这些精妙的设计的意义
    luxor
        61
    luxor  
       2022-05-20 16:34:21 +08:00
    @Biwood 原文最大的问题就是只从源码角度来评判。是不是高效的代码,编译后才能见真章。通常而言,更好的算法也是更高效的,但原文的例子是个例外。我认为主要原因还是例子的逻辑太简单,源码写得再烂,编译器都给优化掉了。因此还是不要过分的炫技,免得打脸。https://godbolt.org/z/6qo3Yqez3
    bthulu
        62
    bthulu  
       2022-05-20 16:49:53 +08:00
    @masterclock 写内核的都是工作之余用爱发电, 在能用的情况下, 当然是怎么快怎么来了
    cnbatch
        63
    cnbatch  
       2022-05-20 17:15:00 +08:00
    既然有人提到了 container_of ,刚好几个星期前在另一个帖子就聊过,我觉得可以再把例子拿出来说一次。

    先来看看 Linux 最经典的用了 GCC 内置扩展的写法:

    #define container_of(ptr, type, member) ({ \
    const typeof(((type *)0)->member) * __mptr = (ptr); \
    (type *)((char *)__mptr - offsetof(type, member)); })

    来源: https://github.com/torvalds/linux/blob/master/tools/include/linux/kernel.h


    然后是最新的 container_of ,经过多年进化,新版本已经不需要用 GCC 内置扩展:

    #define container_of(ptr, type, member) ({ \
    void *__mptr = (void *)(ptr); \
    static_assert(__same_type(*(ptr), ((type *)0)->member) || \
    __same_type(*(ptr), void), \
    "pointer type mismatch in container_of()"); \
    ((type *)(__mptr - offsetof(type, member))); })

    来源: https://github.com/torvalds/linux/blob/master/include/linux/container_of.h


    相信不少人已经相当熟悉了,它的具体原理没必要再解释。



    换换口味,再来看看 FreeBSD 的写法:

    #define container_of(p, stype, field) ((stype *)(((uint8_t *)(p)) - offsetof(stype, field)))

    来源: https://github.com/freebsd/freebsd-src/blob/main/sys/dev/cxgb/cxgb_adapter.h

    一目了然,简洁易懂。


    所以说,想要同时兼顾“更易理解”、“更少行数”,总会有办法的,尽管可能需要等很多年。

    主题所讲的从教科书到 Linux 手动优化,显然也是教科书的做法出现得最早。所以或许以后会有人想得出更易理解但行数也不多的写法吧。
    lslqtz
        64
    lslqtz  
       2022-05-20 17:32:26 +08:00
    不一定所有场合都需要如此优雅是真的,我只能说。。
    iamzuoxinyu
        65
    iamzuoxinyu  
       2022-05-20 17:39:07 +08:00
    > #define container_of(p, stype, field) ((stype *)(((uint8_t *)(p)) - offsetof(stype, field)))

    这个没法保证类型安全吧。
    icyalala
        66
    icyalala  
       2022-05-20 18:15:51 +08:00   ❤️ 1
    @luxor 看了下汇编,确实第一种反而好些。

    我觉得 Linux 强调的是 "a special case goes away and becomes the normal case",那段代码只是个做个例子:"But this is simple code. This is CS 101. This is not important."

    如果说性能的话,我记得很深:
    去年有人翻出来 Linus 第一次参加大型会议的演讲,那时他才 24 岁。他说他自己是 "performance freak",喜欢每隔一段时间就看看 kernel 编译后的汇编结果,然后返回来改 C 原码来得到更好性能,因此实际会出现些不好的代码,因为那是为 gcc 特别改的。
    Kylin30
        67
    Kylin30  
       2022-05-20 18:35:21 +08:00
    优雅快捷键:alt+enter
    cnbatch
        68
    cnbatch  
       2022-05-20 18:51:28 +08:00
    @cnbatch #63 我还是一时大意了,实际上新版 container_of 写法仍然还是用了 GCC 内置扩展 __same_type

    @iamzuoxinyu 需要强制检查的时候,FreeBSD 部分地方仍然提供了依赖扩展功能 typeof 的的版本

    https://github.com/freebsd/freebsd-src/blob/main/sys/dev/drm2/drm_os_freebsd.h

    #define container_of(ptr, type, member) ({ \
    __typeof( ((type *)0)->member ) *__mptr = (ptr); \
    (type *)( (char *)__mptr - offsetof(type,member) );})

    两者的差异就在于是否想彻底实施 C 语言的“哲学”:相信程序员
    smdbh
        69
    smdbh  
       2022-05-20 19:35:01 +08:00
    当初看很兴奋,为了通用化,减少分支。喜欢 if else 的看到嵌套十几层的不会有反感的
    greygoo
        70
    greygoo  
       2022-05-20 20:10:47 +08:00
    你是对的,也看了下 clang 的结果但是两种循环里面的指令都多了一条 lea
    sparky
        71
    sparky  
       2022-05-20 23:26:32 +08:00
    有意思,c 语言吃透指针果然各种玩法
    ericguo
        72
    ericguo  
       2022-05-21 00:14:23 +08:00   ❤️ 1
    那么明显的第二种好,当然我承认我第一次写多半也是第一种。
    lesismal
        73
    lesismal  
       2022-05-21 09:29:11 +08:00
    二级指针看着挺舒服的
    zhzhh
        74
    zhzhh  
       2022-05-21 11:57:36 +08:00
    看了好一会儿才想清楚(我好菜啊哈哈
    pmispig
        75
    pmispig  
       2022-05-21 15:48:51 +08:00
    代码量少有什么好处?优雅的代码有 2 个要素: 1:性能 2:易懂。
    代码量从来不是是否优雅的标准。
    holydancer
        76
    holydancer  
       2022-05-21 17:25:58 +08:00
    看了两遍才看懂 2 的实现,太久没接触 C 了
    完全理解后觉得,指针熟练的人,可能确实写 2 出错概率更低,且效率更高,完全是下意识。
    高级语言用多的人,会下意识排斥 2 的写法。
    junyee
        77
    junyee  
       2022-05-21 20:08:09 +08:00
    * 老大开口,言听必从。

    * 凡文言文必给 0 分。

    C 语言渣渣路过。。。
    ZhiyuanLin
        78
    ZhiyuanLin  
       2022-05-22 01:19:33 +08:00
    觉得 2 可读性不如 1 ,可能是太久没写 C 了……
    指针也不是啥高级特性
    jedihy
        79
    jedihy  
       2022-05-22 05:49:02 +08:00
    2 在 kernel 里面是常规操作了,我自己也是这么写很多年了。
    WispZhan
        80
    WispZhan  
       2022-05-22 09:32:26 +08:00 via Android
    2 确实是目前最优雅的解
    YuuuuuuH
        81
    YuuuuuuH  
       2022-05-22 23:35:23 +08:00
    @enchilada2020 你说的 Stefan 是哪位呢,能否给个链接?谢谢
    enchilada2020
        82
    enchilada2020  
       2022-05-22 23:38:58 +08:00 via Android
    FrankHB
        83
    FrankHB  
       2022-05-23 01:24:50 +08:00
    懒得看视频,不过看原文描述,这个 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
        84
    FrankHB  
       2022-05-23 01:44:37 +08:00
    @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
        85
    FrankHB  
       2022-05-23 02:13:20 +08:00
    ……上面一堆 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
        86
    FrankHB  
       2022-05-23 03:02:15 +08:00
    淦,上面几楼迷惑 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
        87
    cnbatch  
       2022-05-23 04:56:24 +08:00
    @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
        88
    ColorfulBoar  
       2022-05-23 05:36:31 +08:00
    @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
        89
    zinwalin  
       2022-05-23 09:07:13 +08:00
    谢谢分享
    zinwalin
        90
    zinwalin  
       2022-05-23 09:18:23 +08:00
    优雅不一定易读,但程序员知道有更优雅的写法,其实好处很多,在自己的个人项目里就可以用,记得加注释。
    pastor
        91
    pastor  
       2022-05-23 11:36:24 +08:00
    @FrankHB #83

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

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

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

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

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

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

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

    我只是看不惯你们张口就来说别人行业领袖不行,乱带节奏,但我可没乱给你扣帽子舔或者怎么样,所以也请你们逻辑不要那么混乱地乱扣帽子
    浮躁的时候谁都有,遇到机会了扪心自问下改正了提高下自己的修养可能更香
    程序员多数都懂着一些 linux 相关的呢,送人玫瑰手有余香,赞赏肯定一下别人有那么难吗
    pastor
        95
    pastor  
       2022-05-23 12:25:10 +08:00
    @ColorfulBoar 而且,既然 cpper 可以跨界去喷 linus 的 c ,我这种 c++11 前的人评价你 c++11 后的的 c++也相当于跨界了,本质也没什么不同,别扯什么资格不资格的
    ColorfulBoar
        96
    ColorfulBoar  
       2022-05-23 12:51:45 +08:00
    @pastor 所以你真的是啥都不懂……这跟左值右值有什么关系?用引用传参代替指针传参不是 C++98 里面就有的东西?第一天学引用的人都能花 10 秒的时间把它改成用指针的版本,这种东西要花十年……你是不是以为引用的背后有个什么庞大的 runtime 在支持它所以只要用了引用就不可能改写成能进内核的版本了?
    pastor
        97
    pastor  
       2022-05-23 13:00:27 +08:00
    @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
        98
    cnbatch  
       2022-05-23 14:20:20 +08:00
    auto p = list_head(&head);
    右边的 & 符号是取址,一直以来都没变化,到了 C++20 依然还是取址(不考虑运算符重载)

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

    区分很简单,单个 & 的语义从 C++98 以来都没变化。两个 & 的时候才是跟右值引用有关。左值引用跟右值引用的区分直接看 & 的数量就行。
    icyalala
        99
    icyalala  
       2022-05-23 14:31:58 +08:00
    @FrankHB
    "从 Linus Torvalds 这些年来干的活看本质是个 PM ,从不用指望 coding 水平多高就是了。"
    pastor
        100
    pastor  
       2022-05-23 14:32:47 +08:00
    @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:
    1  2  
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1083 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 22:41 · PVG 06:41 · LAX 14:41 · JFK 17:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.