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
去掉了就可以。
这很怪:
- 全局变量可以对外链接。
- 全局常量(非 inline、非 volatile)不能对外链接。
- 全局 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
= C99inline
,绝不生成函数定义。gnu89inline
= C99extern inline
,生成函数定义且对外链接 😅。 - gnu89
static inline
= C99static inline
,函数符号是LOCAL
的。 - C++ 只要带有
inline
(除了普通inline
也包括static inline
和extern inline
)就不会主动生成函数,除非本翻译单元有对此函数的调用。所以 C++static inline
和 gnu89/C99 的static inline
还是有点微小的区别,不过都能防止 ODR。 - C 语言中,
static inline
不会主动生成函数,除非本翻译单元有对此函数的调用。 - C++
extern inline
和inline
效果一样,如果函数被用到则会创建符号定义,但是被标记为WEAK
,可以对外链接,也不怕重复(会从多个版本中选择一个)。
对于 inline 函数的结论:
- 在 C 语言中因为版本语义变化的问题,建议使用
static inline
来处理需要在头文件中包含函数定义的情况(防止 ODR)。 - 在 C++ 中函数使用
inline
即可。在用模板写 traits 的时候可能会用到static inline
。- 一方面,
static
不对外链接意味着可以不必生成函数的代码,是个很好的内联提示。 - 另一方面,
inline
函数被用到时还是会在每个目标文件中生成定义,而且对外链接。C++17 又要求inline
变量 / 函数在其翻译单元内能找到定义,那还不如一步到位写成static inline
了?倒也不是,因为生成的符号是 weak,链接的时候只选择一个定义,有助于减少链接后的文件体积。 - 大家都写
inline
可能是因为效果其实差不多,少写个关键字。
- 一方面,
关于 inline 变量:
- 对于 C++ 变量来说,
inline
好处更大,能实现在类中直接定义static
变量和在头文件中包含非const
的全局变量(C++17 之后才能使用 inline variables)。对于函数来说作用不明显。B 站评论区 提醒了我。 - 对于 C++ 变量来说,extern inline 并不等于 inline:
error: odr-used inline variable 'x' is not defined
,好像是用不了。static inline
和static
的表现相似,会生成 bss 段或者 data 段的 local 变量(从 nm 看分别为 b 和 d 标志;如果是需要函数来初始化的也会放在 bss)。inline
生成的是 undefined local 变量(从 nm 看为 u)。 - Cppreference 的文档 里面没有 C 语言 inline 变量的信息,在 compiler explorer 中测试发现可以定义 inline 变量,但效果和不加 inline 效果一样,看似是被忽略了。