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

C++ 利用条件编译来避免重复引用为什么需要手写?编译器不能自动做这件事情吗?

  •  
  •   vcfghtyjc · 2022-03-25 13:33:13 +08:00 · 3289 次点击
    这是一个创建于 735 天前的主题,其中的信息可能已经有所发展或是发生改变。

    继续读《 C++ Primer 》,看到 2.6 中介绍了利用条件编译来避免重复引用。以下是源码:

    #ifndef SALES_DATA_H
    #define SALES_DATA_H
    #include <string>
    struct Sales_data {
      std::string bookNo;
      unsigned units_sold = 0;
      double revenue = 0.0;
    };
    #endif
    

    所以理论上来说,是不是每个自定义的数据结构都应该加上这个判断,从而避免重复引用?为什么编译器不能自动做这个事情?重复引用会带来什么样的问题?

    29 条回复    2022-04-23 22:07:03 +08:00
    GeruzoniAnsasu
        1
    GeruzoniAnsasu  
       2022-03-25 13:36:29 +08:00   ❤️ 3
    说一个会的人都知道但没学过可能无法想象的事:

    c++的编译器只会处理「一个文件」,include 的作用是把所有<>里的文件全都拼在一起



    字面意思,拼在一起。include 两次就拼两次。
    mainjzb
        2
    mainjzb  
       2022-03-25 13:41:45 +08:00
    微软干过这种事情.发明了
    #progma once

    后来人们发现头文件是个憨批设定,严重影响了编译速度。已经在 C++20 里开始推进 module 了。
    vcfghtyjc
        3
    vcfghtyjc  
    OP
       2022-03-25 13:44:51 +08:00
    @mainjzb 为啥头文件“严重”影响编译速度?我以为只有源码长度影响,毕竟头文件一般只是定义并没有实现。
    msg7086
        4
    msg7086  
       2022-03-25 13:45:20 +08:00
    重复引用会冲突。
    但是自动过滤重复引用以后又等于是扼杀了故意多次引用文件的能力。
    所以你只能每次老老实实写上 #pragma once 来告诉编译器你不想重复引用。
    GeruzoniAnsasu
        5
    GeruzoniAnsasu  
       2022-03-25 13:46:55 +08:00
    能让编译器「自己做这个事情」的提案叫 modules ,是一个有近 10 年历史的卫星,当然这跟 c++存在的漫长时间相比并不算什么

    https://en.cppreference.com/w/cpp/compiler_support 在这里搜 modules 有惊喜

    p.s. 别看这个 proposal 是 2019 年的,但其实随便一搜就能找到过去的弃案比如 https://isocpp.org/files/papers/n4214.pdf
    ho121
        6
    ho121  
       2022-03-25 13:57:25 +08:00
    如果你真的需要引用两次呢?
    Origami404
        7
    Origami404  
       2022-03-25 14:00:50 +08:00 via Android   ❤️ 2
    @vcfghtyjc 因为头文件会展开啊,比如有一个 common.h 被 1000 个 .c 文件 include 了,那么编译器就必须处理这个文件里的代码 1000 次(因为 include 是预处理器指令,它的实现就是直接展开)。如果头文件里再来一点深点的 include ,那就是滚雪球了
    mainjzb
        8
    mainjzb  
       2022-03-25 14:01:10 +08:00   ❤️ 1
    头文件比你想象的大的多的多。之前还看到一个 githu 上的神人,推崇库代码全在一个.h 文件里。#include 进来就能用,避免各种编译链接问题。
    😅想象一下。a.h 包含 b.h , b.h 包含 c.h 你的文件写了 10 行,引入了 a.h 结果最后编译器导入进来复制了 a.h b.h c.h 的代码,发现有 10w 行。这是在一个没有固态的年代里很容易发生的一件事情。
    vcfghtyjc
        9
    vcfghtyjc  
    OP
       2022-03-25 14:01:33 +08:00
    @ho121 为啥会需要引用两次?引用一次后不就可以编译了吗?
    vcfghtyjc
        10
    vcfghtyjc  
    OP
       2022-03-25 14:04:04 +08:00
    @Origami404 编译器不能 cache 一下每个文件的处理结果,遇到相同的文件直接用之前处理好的结果吗?
    shyrock
        11
    shyrock  
       2022-03-25 14:05:48 +08:00
    嗯,所以说锅在头文件。
    重复引用什么的都是后话。。。
    mainjzb
        12
    mainjzb  
       2022-03-25 14:06:45 +08:00
    你说的编译器 cache 叫预编译头文件。事实就是还是很慢。一堆人还在那疯狂用宏写函数啥的。
    Cloutain
        13
    Cloutain  
       2022-03-25 14:10:14 +08:00
    兼容 C 语言的烂包袱,你看 C#还有这破事儿吗?
    vcfghtyjc
        14
    vcfghtyjc  
    OP
       2022-03-25 14:11:51 +08:00
    @mainjzb 那能不能编译前自动给每个 data structure 都加个判断,除非声明就是要重复引用?
    DOLLOR
        15
    DOLLOR  
       2022-03-25 14:20:59 +08:00   ❤️ 1
    因为 C/C++里的 include 就是单纯地把其他文件里的代码 copy 到当前文件里进行编译,所以必须手动地写一些指令,防止 copy 后出现冲突。
    为什么编译器不能自动做这个事情?人家也想呀,就是所谓的模块化,一直在缓慢推进。
    但普及需要时间,就像 JS 的 es module 一样,C++可能会有很长的一段时间,还得用这种原始的方式来 include 文件。
    GeruzoniAnsasu
        16
    GeruzoniAnsasu  
       2022-03-25 14:23:47 +08:00
    @vcfghtyjc
    > 为啥会需要引用两次

    你先再看一眼 1L ,然后看这个
    https://onlinegdb.com/b4qgRG-ZS


    哎,你别说以前可能还真有人认为会有需要 include 两次的地方
    neoblackcap
        17
    neoblackcap  
       2022-03-25 14:24:47 +08:00   ❤️ 1
    @vcfghtyjc 你说得很好,不要再说了,再说就要请你去写 C++编译器了

    因为 C++的编译器继承 C 编译器的特点,可以独立编译文件,这样每一个文件就是一个编译单元,可以分布式编译。编译的时候,编译器是不知道一个符号在全局中有没有被编译的。能知道全局符号信息的那个叫连接器。所以人们为了避免重复编译,所以就在头文件写宏,防止多次编译(不是多次 include )。
    因为只要编译了,连接器就能在连接的过程中找到对应的符号的实现。
    ipwx
        18
    ipwx  
       2022-03-25 14:26:03 +08:00   ❤️ 4
    @vcfghtyjc 你说对了,源码长度就是很长,而且还很复杂。

    纯 C 语言的头文件一般只有声明所以速度相对快,但是 C++ 不同。为了追求 zero-cost abstraction ,C++ 的很多东西声明和定义全部都在头文件里面,而且模板 meta-programming 对于编译器是复杂的东西。这样叠加上去就,超级慢。一个 C++ 源代码文件 include 展开以后有几百上千 KB ,几百上千个 template class ,我觉得毫不意外。每个源文件都给你来这一套,想想多复杂。

    C++20 的 module 就是规定,一些头文件不受其他头文件 macro 的影响。这样你就能预编译这些头文件,就不会有那么多重新编译的开销了。

    在 C++20 module 前,业界最佳的实践是 Qt ,大量使用 private class + pointer ,尽量避免使用 template ,达到了很好的编译速度。然而,由于使用这两个技术,所以毕竟不是 zero-cost abstraction ,因此在运行速度上是打折了的。当然比起 node.js 还是秒杀。

    最后简要介绍 zero-cost abstraction 。这东西看一个例子:

    * C 语言的库函数 qsort 要传入一个函数指针。
    * C++ 的库函数 std::sort 传入的是模板函数。

    函数指针是不能内联优化的,因此每两个元素比较都不得不进行一次函数调用,有固有性能损失。
    模板函数是可以内联优化的,相当于消除了这一次函数调用。更何况消除以后 C++ 可以进一步做指令集优化。

    所以 C 语言 qsort 比 C++ 慢。
    ipwx
        19
    ipwx  
       2022-03-25 14:27:08 +08:00   ❤️ 2
    @vcfghtyjc 编译器不能 cache 一下每个文件的处理结果,遇到相同的文件直接用之前处理好的结果吗?
    ----

    这就是 C++20 module ,但是前提是要承认“一个头文件里面的行为不受它之前 include 的其他头文件影响”。

    现在的编译流程不具有这个约定。你八竿子搭不上边的 #define 可以影响到后面 include 的头文件。
    ipwx
        20
    ipwx  
       2022-03-25 14:29:45 +08:00   ❤️ 2
    @vcfghtyjc 那能不能编译前自动给每个 data structure 都加个判断,除非声明就是要重复引用?
    ----

    编译器其实没那么弱鸡,C 语言的头文件 include 也挺快。C++ 之所以那么慢还是因为生命定义全部放进头文件了,而且全都是超级复杂的模板。meta-programming 你学习一下就知道多强大了,这玩意儿可是编译期就图灵完全的。

    相当于你能在代码里写递归、写斐波那契数列、写 whatever 东西,让编译期在编译的时候把结果算出来,而不是在运行的时候算出来。当然,前提是你写得出来。
    masterclock
        21
    masterclock  
       2022-03-25 14:31:02 +08:00   ❤️ 1
    @vcfghtyjc 引用两次的情况存在啊
    见过一些代码,define 个东西,include 一个文件,然后 define 另一个东西,再一次 include ,这样就能包含进来不一样的东西了
    codehz
        22
    codehz  
       2022-03-25 14:31:49 +08:00   ❤️ 2
    多次 include 是有意义的( c 里用的比较多)
    可以起到类似代码生成的作用
    你可以在某个文件里放一堆类似 GEN(a, int, "some text")的“语句”,然后在不同的地方 include 的时候定义 GEN 宏展开成不同的东西(比如一边展开成 enum ,另一边展开成 switch case ,就可以实现 enum 值打印)
    C++显然没必要打破这种兼容性,自然就保留下来了
    3dwelcome
        23
    3dwelcome  
       2022-03-25 15:06:59 +08:00   ❤️ 1
    @vcfghtyjc "那能不能编译前自动给每个 data structure 都加个判断,除非声明就是要重复引用?"

    因为做不到啊。C 编译器的宏预处理和实体编译模块是分两步进行的。

    也就是#include 会忠实执行内联文件的责任,先把所有头文件都塞到一个巨大的临时 C 文件里,再给编译器。

    编译器在编译时候,仅仅从临时 C 文件里看,看不出 data structure 定义是不是来自同一个头文件。如果知道,那肯定是能自动去重复的。

    同样是编译程序,这点还是 java 设计好。C 有太多历史负担了。
    ysc3839
        24
    ysc3839  
       2022-03-25 16:10:00 +08:00   ❤️ 1
    @3dwelcome 从标准看是不可以,但是主流实现都支持 #pragma once 了,实际是可以
    msg7086
        25
    msg7086  
       2022-03-25 17:35:23 +08:00   ❤️ 1
    头文件在设计的时候就是做成像复制粘贴一样引入源码文件里的。
    比如你头文件里写一个 printf 语句,然后在函数里每次#include 这个头文件就相当于把这个 printf 语句复制粘贴到代码里一样,你引用 3 次就等于复制粘贴了 3 次语句进来。
    因为头文件本来实现的就是复制粘贴功能,所以很难直接处理语言本身的含义。

    简单来说就是 Ctrl-V 这个功能为什么要的管你 Ctrl-V 了几次呢。

    再举一个现实中会用到的例子。比如你写了一个头文件,封装在一个命名空间里,然后把命名空间做成宏。
    那么你就可以用类似:
    #define NS ns1
    #include <code.h>
    #undef NS
    #define NS ns2
    #include <code.h>
    #undef NS
    这样前后生成两个不同命名空间的两套代码。
    x265 就是用类似的方法把 8bit 10bit 12bit 的编码器分别装在三个命名空间里然后链接在一起的。
    ligiggy
        26
    ligiggy  
       2022-03-26 10:02:51 +08:00
    c++永远都这么有意思, full of fun 。
    seakingii
        27
    seakingii  
       2022-03-26 11:14:36 +08:00
    最讨厌 C,C++的头文件啥的

    C,C++的编译问题很多,所以新一点的语言,比如 RUST, GOLANG 都有一部分责任要解决 C,C++编译的问题
    troywinter
        28
    troywinter  
       2022-03-26 13:21:46 +08:00
    你说的这玩意有两个实现,一个叫 java ,一个叫 c#,直接用就行了
    fawdlstty
        29
    fawdlstty  
       2022-04-23 22:07:03 +08:00
    @vcfghtyjc 真有可能引用两次并且以不同方式引用。参见里面我备注的 xmh 大佬的答案
    https://www.zhihu.com/question/456804478
    作为头文件引用一次,可以调用里面的函数;作为字符串引用一次,可以做字符串处理
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3264 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 38ms · UTC 10:47 · PVG 18:47 · LAX 03:47 · JFK 06:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.