inline 函数不对外链接?gnu89 和 c99 恐怖的语义对换!

经过

遇到过一个坑:为共享库写函数,但是又需要从头文件隐藏实现时,不要将函数声明为内联。否则编译器会认为它未被使用并忽略它,链接的时候就找不到这个函数。

项目使用的语言是 C++。

TL;DR

其实只是不会主动生成函数定义罢了,不是不能对外链接。除非调用处看到的声明带有 inline 关键字,从而尝试在其所在的翻译单元中寻找。

尝试解释:C++ 中内联函数按需生成

https://zh.cppreference.com/w/cpp/language/inline 中有一句:

内联函数或变量 (C++17 起) 的定义必须在访问它的翻译单元中可达(不一定要在访问点前)。

C++ 外部链接的内联函数 / 变量是什么意思?

文档中有这句话:

Inline const variables at namespace scope have external linkage by default (unlike the non-inline non-volatile const-qualified variables).

证实后半句:

// main.cc
#include <iostream>

const int a = 3;
void f();

int main() {
  f();
  printf("lalala\n");
}

// linkme.cc
#include <cstdio>
extern int a;

void f() { printf("a=%d\n", a); }

以上代码 linkme.cc 是无法成功链接 a 的。但是把 const 去掉了就可以。

这很怪

  1. 全局变量可以对外链接。
  2. 全局常量(非 inline、非 volatile)不能对外链接
  3. 全局 inline 常量可以对外链接。

还有这一句话:

An inline function or variable (since C++17) with external linkage (e.g. not declared static)…

C++ 是允许不同翻译单元中出现同一 inline 函数或者变量的,但是它们的地址在每个翻译单元中都相同、声明在每个翻译单元中都必须标记 inline(在 gcc 12.2 上做测试,有个源文件没有标 inline 也没有报错)、同时定义也应该相同(如果不同则程序非良构,但编译器不需要报错),这样才能保证多处定义可以合并。

inline 变量是否按需生成(C 和 C++ 不同)

eric@debian:~/linkage$ echo 'inline int a = 6;' > inline.cc
eric@debian:~/linkage$ echo 'inline int a = 6;' > inline.c
eric@debian:~/linkage$ echo 'int a = 6;'        > outofline.cc
eric@debian:~/linkage$ gcc -c inline.c -o inline.c.o
inline.c:1:12: warning: variable ‘a’ declared ‘inline’
    1 | inline int a = 6;
      |            ^
eric@debian:~/linkage$ gcc -c inline.cc -o inline.cc.o
eric@debian:~/linkage$ gcc -c outofline.cc
eric@debian:~/linkage$ ls -alh
total 32K
drwxr-xr-x  2 eric eric 4.0K Jul 22 16:14 .
drwx------ 10 eric eric 4.0K Jul 22 16:07 ..
-rw-r--r--  1 eric eric   18 Jul 22 16:13 inline.c
-rw-r--r--  1 eric eric   18 Jul 22 16:13 inline.cc
-rw-r--r--  1 eric eric  800 Jul 22 16:14 inline.cc.o
-rw-r--r--  1 eric eric  840 Jul 22 16:14 inline.c.o
-rw-r--r--  1 eric eric   11 Jul 22 16:13 outofline.cc
-rw-r--r--  1 eric eric  840 Jul 22 16:14 outofline.o

可以发现在 C 语言中给 a 变量加了 inline 但没有使用到,编译器给了警告(在 C++ 中没有)。还发现 inline.cc.o 和 inline.c.o 的大小不同!

eric@debian:~/linkage$ readelf -s inline.c.o | grep OBJECT
     2: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 a
eric@debian:~/linkage$ readelf -s inline.cc.o | grep OBJECT
eric@debian:~/linkage$ readelf -s outofline.o | grep OBJECT
     2: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 a

上面的测试也说明在 C++ 中,没有被使用到的 inline 变量是真的被优化掉了,连符号表中都找不到。

结论:C 语言中 inline 变量总是生成(我测试时候语言标准估计是 C99)。C++ 中 inline 变量按需生成。看符号表就知道了,如果生成了就是可以对外链接的。

inline 函数没用到就不会生成

eric@debian:~/tlpi/linkage$ echo 'inline int f(int x) { return x; }' > inline.cc
eric@debian:~/tlpi/linkage$ echo 'inline int f(int x) { return x; }' > inline.c
eric@debian:~/tlpi/linkage$ echo 'int f(int x) { return x; }' > outofline.cc
eric@debian:~/tlpi/linkage$ gcc -c inline.c -o inline.c.o
eric@debian:~/tlpi/linkage$ gcc -c inline.cc -o inline.cc.o
eric@debian:~/tlpi/linkage$ gcc -c outofline.cc
eric@debian:~/tlpi/linkage$ ls -alh
total 32K
drwxr-xr-x 2 eric eric 4.0K Jul 22 16:27 .
drwxr-xr-x 9 eric eric 4.0K Jul 22 16:24 ..
-rw-r--r-- 1 eric eric   34 Jul 22 16:26 inline.c
-rw-r--r-- 1 eric eric   34 Jul 22 16:26 inline.cc
-rw-r--r-- 1 eric eric  800 Jul 22 16:27 inline.cc.o
-rw-r--r-- 1 eric eric  800 Jul 22 16:27 inline.c.o
-rw-r--r-- 1 eric eric   27 Jul 22 16:27 outofline.cc
-rw-r--r-- 1 eric eric 1.1K Jul 22 16:27 outofline.o

可以发现 outofline.o 和 inline.c.o 或者 inline.cc.o 二点大小不同。果然 outofline.o 的符号表中有函数 f()(名字重整后为 _Z1fi

eric@debian:~/tlpi/linkage$ readelf -s outofline.o

Symbol table '.symtab' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS outofline.cc
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    12 FUNC    GLOBAL DEFAULT    1 _Z1fi
eric@debian:~/tlpi/linkage$ readelf -s inline.cc.o

Symbol table '.symtab' contains 2 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS inline.cc

再试试 C 语言,没有把函数 f() 内联时果然也生成了代码:

eric@debian:~/tlpi/linkage$ echo 'int f(int x) { return x; }' > outofline.c
eric@debian:~/tlpi/linkage$ gcc -c outofline.c
eric@debian:~/tlpi/linkage$ readelf -s outofline.o

Symbol table '.symtab' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS outofline.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    12 FUNC    GLOBAL DEFAULT    1 f

结论:inline 函数没用到就不会生成

inline 函数是否可以对外链接(C 和 C++ 不同)

函数 f() 是 inline 函数,但是 g() 是非 inline 的,所以 g() 的代码一定会生成,同时还会迫使 f() 的代码生成。

eric@debian:~/tlpi/linkage$ vim inline.c
eric@debian:~/tlpi/linkage$ cat inline.c
inline int f(int x) { return x; }

int g(int x) { return f(x); }
eric@debian:~/tlpi/linkage$ cat inline.c > inline.cc
eric@debian:~/tlpi/linkage$ gcc -c inline.cc -o inline.cc.o
eric@debian:~/tlpi/linkage$ gcc -c inline.c -o inline.c.o
eric@debian:~/tlpi/linkage$ readelf -s inline.c.o

Symbol table '.symtab' contains 5 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS inline.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    23 FUNC    GLOBAL DEFAULT    1 g
     4: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND f
eric@debian:~/tlpi/linkage$ readelf -s inline.cc.o

Symbol table '.symtab' contains 6 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS inline.cc
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 .text._Z1fi
     4: 0000000000000000    12 FUNC    WEAK   DEFAULT    6 _Z1fi
     5: 0000000000000000    23 FUNC    GLOBAL DEFAULT    2 _Z1gi

从这个结果看来 C 语言中 inline 函数是不对外链接的(Type=NOTYPE、Ndx=UND),但是 C++ 中 inline 函数是对外链接的(Type=FUNC、Ndx=6),只是绑定类型为 WEAK(如果有多个弱引用链接器会选一个、如果有强引用链接器会忽略弱引用)。

在 C++ 中果然是可以成功链接 f 的:

# 注意 -xc++ 一定要放在最后,不然目标文件 inline.cc.o 也会被当成 C++ 源码处理从而失败
gcc inline.cc.o -o a.cc.out -xc++ <(echo 'int f(int); int main() { return f(0); }')

在 C 语言的那个测试就比较怪了,内联的 f() 函数代码在用到的情况下也根本就没有生成!用 objdump -D inline.c.o 只能看到 g() 函数的代码。

根据 Stack Overflow 问题 what-does-extern-inline-do,GNU89、GNU99/C99 和 C++ 中 inline 关键字的行为不同。我又尝试了不同的 C 语言版本:

eric@debian:~/tlpi/linkage$ gcc -std=c99 -c inline.c -o inline.c.o
eric@debian:~/tlpi/linkage$ readelf -s inline.c.o

Symbol table '.symtab' contains 5 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS inline.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    23 FUNC    GLOBAL DEFAULT    1 g
     4: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND f
eric@debian:~/tlpi/linkage$ gcc -std=gnu99 -c inline.c -o inline.c.o
eric@debian:~/tlpi/linkage$ readelf -s inline.c.o

Symbol table '.symtab' contains 5 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS inline.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    23 FUNC    GLOBAL DEFAULT    1 g
     4: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND f
eric@debian:~/tlpi/linkage$ gcc -std=gnu89 -c inline.c -o inline.c.o
eric@debian:~/tlpi/linkage$ readelf -s inline.c.o

Symbol table '.symtab' contains 5 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS inline.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    12 FUNC    GLOBAL DEFAULT    1 f
     4: 000000000000000c    23 FUNC    GLOBAL DEFAULT    1 g

发现在 gnu89 的时候,f() 函数被生成了,再把目标文件拿去链接成可执行文件也是可以成功的。在 C99 的时候,inline(不是 static inline / extern inline)有点像声明了。而且不同版本之间语义发生了变化,理解起来非常棘手。

根据 https://stackoverflow.com/a/77852086/ ,C99 中的函数被标记为 inline(没有 static / extern) 时,编译器可以内联也可以不内联这个函数,如果编译器选择不内联,则目标代码若需要调用这个函数,会直接使用这个函数符号,但并不生成它的代码。即:C99 函数有 inline 没 static 会阻止当前翻译单元中独立生成该函数!因此必须在其他文件中有一个此函数定义。(这似乎提供了一种机制,在内联 / 不内联的时候可以使用两份略微不同的代码定义?)在 C++ 中,inline 函数代码总是生成在每个翻译单元中,并由链接器负责丢弃重复的定义。


2025/3/8 https://gcc.gnu.org/onlinedocs/gcc/Inline.html GCC 的文档也提供了一些信息,在 The remainder of this section is specific to GNU C90 inlining. C89 和 C90 是一回事。这句话的下面:

这一段说的是 inline,光是 inline 不能防止违反 ODR,也就是说 C90 的 inline 生成定义且对外链接。When an inline function is not static, then the compiler must assume that there may be calls from other source files; since a global symbol can be defined only once in any program, the function must not be defined in the other source files, so the calls therein cannot be integrated. Therefore, a non-static inline function is always compiled on its own in the usual fashion.

这一段开始说的是 extern inline,在本文件能找到定义则有内联的可能,但是如果不内联则会直接调用函数且不生成其定义。和 inline 相比 extern inline 不会生成定义所以没有对外链接的问题。If you specify both inline and extern in the function definition, then the definition is used only for inlining. In no case is the function compiled on its own, not even if you refer to its address explicitly. Such an address becomes an external reference, as if you had only declared the function, and had not defined it.

This combination of inline and extern has almost the effect of a macro. The way to use it is to put a function definition in a header file with these keywords, and put another copy of the definition (lacking inline and extern) in a library file. The definition in the header file causes most calls to the function to be inlined. If any uses of the function remain, they refer to the single copy in the library.

看下面的代码,在 gnu89 中 inline 关键字生成了代码,也就是说对外链接、不能防止违反 ODR!

➜  inline cat lib.c 
inline int f() {
    return 0;
}                                                                                                              
➜  inline cat main.c 
int f();

int main() {
    return f();
}
➜  inline gcc -std=gnu89 -o lib.o -c lib.c && gcc -o main main.c lib.o && ./main
➜  inline gcc -std=c99 -o lib.o -c lib.c && gcc -o main main.c lib.o && ./main
/usr/bin/ld: /tmp/cc7ODays.o: in function `main':
main.c:(.text+0xe): undefined reference to `f'
collect2: error: ld returned 1 exit status
➜  inline gcc -std=gnu99 -o lib.o -c lib.c && gcc -o main main.c lib.o && ./main
/usr/bin/ld: /tmp/ccPN2lZj.o: in function `main':
main.c:(.text+0xe): undefined reference to `f'
collect2: error: ld returned 1 exit status

inline 出现于 GNU 方言、C99、C++ 中。在 C99 之前的版本中如果不用方言(比如有 -ansi 或者 -std)就没有 inline 关键字,这个时候可以用 __inline__。亲测 -std=gnu89 可以用 inline 但是 -std=c89 不能。

观察:

  • gnu89 extern inline = C99 inline,绝不生成函数定义。gnu89 inline = C99 extern inline,生成函数定义且对外链接 😅。
  • gnu89 static inline = C99 static inline,函数符号是 LOCAL 的。
  • C++ 只要带有 inline(除了普通 inline 也包括 static inlineextern inline)就不会主动生成函数,除非本翻译单元有对此函数的调用。所以 C++ static inline 和 gnu89/C99 的 static inline 还是有点微小的区别,不过都能防止 ODR。
  • C 语言中,static inline 不会主动生成函数,除非本翻译单元有对此函数的调用。
  • C++ extern inlineinline 效果一样,如果函数被用到则会创建符号定义,但是被标记为 WEAK,可以对外链接,也不怕重复(会从多个版本中选择一个)。

对于 inline 函数的结论:

  1. 在 C 语言中因为版本语义变化的问题,建议使用 static inline 来处理需要在头文件中包含函数定义的情况(防止 ODR)。
  2. 在 C++ 中函数使用 inline 即可。在用模板写 traits 的时候可能会用到 static inline
    • 一方面,static 不对外链接意味着可以不必生成函数的代码,是个很好的内联提示。
    • 另一方面,inline 函数被用到时还是会在每个目标文件中生成定义,而且对外链接。C++17 又要求 inline 变量 / 函数在其翻译单元内能找到定义,那还不如一步到位写成 static inline 了?倒也不是,因为生成的符号是 weak,链接的时候只选择一个定义,有助于减少链接后的文件体积
    • 大家都写 inline 可能是因为效果其实差不多,少写个关键字。

关于 inline 变量:

  1. 对于 C++ 变量来说,inline 好处更大,能实现在类中直接定义 static 变量和在头文件中包含非 const 的全局变量(C++17 之后才能使用 inline variables)。对于函数来说作用不明显。B 站评论区 提醒了我。
  2. 对于 C++ 变量来说,extern inline 并不等于 inline:error: odr-used inline variable 'x' is not defined,好像是用不了。static inlinestatic 的表现相似,会生成 bss 段或者 data 段的 local 变量(从 nm 看分别为 b 和 d 标志;如果是需要函数来初始化的也会放在 bss)。inline 生成的是 undefined local 变量(从 nm 看为 u)。
  3. Cppreference 的文档 里面没有 C 语言 inline 变量的信息,在 compiler explorer 中测试发现可以定义 inline 变量,但效果和不加 inline 效果一样,看似是被忽略了。