为什么将函数声明为内联但没有定义会导致链接器错误?

CPP*_*PPL 6 c++ inline function

我知道声明为的函数inline必须在其自己的翻译单元中有定义(正如标准所说:内联函数应在使用它的每个翻译单元中定义)。

以前,我对这个要求的理解是,编译器会尝试内联用inline关键字声明的函数,因此虽然内联不是强制性的,但定义首先应该对编译器可见。

根据这种理解,我认为不提供定义应该会导致编译器错误。事实上,我从来没有编写过inline没有定义的函数。但后来我在我的玩具程序中故意尝试了这个错误,它导致了链接器错误。

所以,我只是好奇为什么 C++ 不将这个问题保留为编译时错误并让编译器捕获该错误?而且,根据我的理解,inline函数默认具有外部链接,因此链接器到底如何在幕后捕获此错误?


一个例子:

// main.cpp:

inline void InlineFun();

int main()
{
    InlineFun();
}

// other.cpp:

#include<iostream>
#include<string>

inline void InlineFun()
{
    std::cout << "From other TU" << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

我在 Windows 10 上使用 Visual Studio 2022 和 C++20。我遇到的错误是LNK2001LNK1120。评论中提供的示例也非常好。


后续示例:

// main.cpp:

inline void InlineFun();

int main()
{
    InlineFun();  // From other TU
}

// other.cpp:

#include<iostream>
#include<string>

inline void InlineFun()
{
    std::cout << "From other TU" << std::endl;
}

void Fun()
{
    InlineFun();
}
Run Code Online (Sandbox Code Playgroud)

这次,使用相同的环境,我添加了一个正常的函数Funother.cpp尝试强制InlineFun具有可观察到的效果。然后,我的程序成功编译和链接(在 Visual Studio 上没有报告任何警告或错误)并打印From other TU,似乎无视标准。

Pet*_*ica 5

的主要作用inline是允许同一函数的多个定义(因为定义通常位于多个翻译单元中包含的标头中)。

\n

“inline”这个名字是一种转移注意力的说法;就像提示一样register,除了“接受多个定义”效果之外,它对于现代编译器来说已经过时了。

\n

现代编译器选择内联或不完全独立于关键字的存在inline

\n

鉴于 的非强制性质inline,构建系统尝试将缺少定义的函数视为非内联函数(在发出强制诊断消息之后)并尝试在链接时解析该符号是有意义的。

\n

不会发生链接器错误,因为调用站点缺少函数定义;正如您所观察到的,这是编译器的域。这是两个文件的简短记录,一个定义了一个声明为内联的函数,一个定义了一个内联函数,我们将在第二步中将其更改为普通函数,以证明这是可行的。我编译两者并使用nm. nm检查目标文件;我们用它来显示“全局符号”(导出的或未解析的)。“T”表示该函数是在“文本”(即代码部分)中定义的,正如人们所期望的那样。

\n
$ cat other.cpp\ninline int InlineFun() { return 3; }\nint Fun() { return InlineFun(); }\n$ gcc -c -O1 other.cpp\n$ nm -g --defined-only other.o\n0000000000000000 T _Z3Funv\n$ cat perhaps-inline.cpp\ninline int InlineFun();\nint main(){ return InlineFun(); /* From other TU */ }\n$ gcc -O1 -c perhaps-inline.cpp\nperhaps-inline.cpp:1:12: warning: inline function \xe2\x80\x98int InlineFun()\xe2\x80\x99 used but never defined\n    1 | inline int InlineFun();\n      |            ^~~~~~~~~\n$ g++ -O1 other.o perhaps-inline.o -o perhaps-inline\n/usr/lib/gcc/x86_64-pc-msys/13.2.0/../../../../x86_64-pc-msys/bin/ld: perhaps-inline.o:perhaps-inline:(.text+0xa): undefined reference to `InlineFun()\'\ncollect2: error: ld returned 1 exit status\n\n
Run Code Online (Sandbox Code Playgroud)\n

声明为内联的函数作为全局符号不可见,并且不可用于链接。事实上,检查目标代码可以确认根本没有为其生成任何代码

\n

InlineFunc()如果没有内联定义,gcc 将in的调用main()视为常规函数调用,并带有警告;错误只是没有提供这样的功能。让我们改变这一点。我们将消除inlineother.cpp 中函数定义中的 ,使其成为常规函数:

\n
$ cat other.cpp\nint InlineFun() { return 3; }\nint Fun() { return InlineFun(); }\n$ gcc -c -O1 other.cpp\n$ nm -g --defined-only other.o\n0000000000000006 T _Z3Funv\n0000000000000000 T _Z9InlineFunv\n$ g++ -O1 other.o perhaps-inline.o -o perhaps-inline\n$ g++ -O1 other.o perhaps-inline.o -o perhaps-inline\n$ ./perhaps-inline\n$ echo $?\n3\n
Run Code Online (Sandbox Code Playgroud)\n

瞧:有效。

\n

您的示例中确实发生了链接器错误,因为该函数是在定义站点中声明为内联的。

\n

该符号根本就没有创建。要成功链接,请删除inline定义函数的位置。(我们看到 gcc 不会以任何特殊方式“修饰”内联函数的名称。)

\n
\n

让我们深入了解一下我手头的两个编译器的标准措辞和行为。

\n

C++ 2020 标准在 6.3/11 中要求:

\n
\n

内联函数或变量的定义应可从每个定义域的末尾进行访问,在该定义域中,内联函数或变量的定义在废弃语句之外进行了 odr 使用。

\n
\n

这里有一点行话:

\n
    \n
  • 如果没有模块,定义域就是翻译单元。
  • \n
  • odr-used: “Odr”代表“一个定义规则”,即变量等在每个翻译单元中只能定义一次。本章规定了哪些用途属于该类别。我不太明白那种不透明的语言,但我确信函数调用可以。
  • \n
  • reachable这个措辞很有趣:它并没有说翻译单元必须包含定义!详情请见答案末尾。
  • \n
  • 丢弃的语句是模板或 const 表达式的一部分,可以在编译时丢弃。
  • \n
\n

正如我们将看到的,“可达”给编译器构建者一些余地。这是一个带有修改示例的 msys g++ 13.2 的指导性会话(特别是,我没有使用 iostream lib,而只是从函数中返回一些内容,包括 main,作为可观察的行为)。我显示这两个文件,将它们编译为目标文件,并使用 gnu binutil 检查它们nm。它表明,尽管InlineFun()有内联声明,但它在调用站点编译为正常的未定义符号,并在实现站点编译为已定义符号。因此,两者可以成功链接:

\n
$ cat perhaps-inline.cpp\ninline int InlineFun();\nint main(){ return InlineFun(); /* From other TU */ }\n$ cat other.cpp\ninline int InlineFun() { return 3; }\nint Fun() { return InlineFun(); }\n$ gcc -c perhaps-inline.cpp other.cpp\nperhaps-inline.cpp:1:12: warning: inline function \xe2\x80\x98int InlineFun()\xe2\x80\x99 used but never defined\n    1 | inline int InlineFun();\n      |            ^~~~~~~~~\n$ nm -g --undefined-only perhaps-inline.o\n                 U _Z9InlineFunv\n                 U __main\n$ nm -g --defined-only other.o\n0000000000000000 T _Z3Funv\n0000000000000000 T _Z9InlineFunv\n$ g++ -o  perhaps-inline other.o perhaps-inline.o\n$ ./perhaps-inline\n$ echo $?\n3\n
Run Code Online (Sandbox Code Playgroud)\n

现在我打开优化:

\n
$ rm *.o perhaps-inline\n$ gcc -O1 -c perhaps-inline.cpp other.cpp\nperhaps-inline.cpp:1:12: warning: inline function \xe2\x80\x98int InlineFun()\xe2\x80\x99 used but never defined\n    1 | inline int InlineFun();\n      |            ^~~~~~~~~\n$ nm -g --defined-only other.o\n0000000000000000 T _Z3Funv\n$ g++ -o  perhaps-inline other.o perhaps-inline.o\n/usr/lib/gcc/x86_64-pc-msys/13.2.0/../../../../x86_64-pc-msys/bin/ld: perhaps-inline.o:perhaps-inline:(.text+0xa): undefined reference to `InlineFun()\'\ncollect2: error: ld returned 1 exit status\n
Run Code Online (Sandbox Code Playgroud)\n

哎呀。该符号InlineFun在 other.o 中消失了。因此,链接器无法再解析 Maybe-inline.o 所需的符号。

\n

正如我们上面看到的,我们可以简单地省略 other.cpp 中函数定义中的“inline”;将发出功能符号并且链接成功。我们还可以告诉编译器不要优化 ( -O0)。它根本不会内联并生成函数代码,包括符号。链接将成功:

\n
$ gcc -O0 -c other.cpp\n$ cat other.cpp\ninline int InlineFun() { return 3; }\nint Fun() { return InlineFun(); }\n$ gcc -O0 -c other.cpp\n$ nm -g --defined-only other.o\n0000000000000000 T _Z3Funv\n0000000000000000 T _Z9InlineFunv\n$ g++ -o perhaps-inline perhaps-inline.o other.o\n$ ./perhaps-inline\n$ echo $?\n3\n
Run Code Online (Sandbox Code Playgroud)\n

或者更短,首先 with-O0成功,并出现警告,然后 with -O1,在链接阶段失败:

\n
$ g++ -O0 -o perhaps-inline other.cpp perhaps-inline.cpp\nperhaps-inline.cpp:1:12: warning: inline function \xe2\x80\x98int InlineFun()\xe2\x80\x99 used but never defined\n    1 | inline int InlineFun();\n      |            ^~~~~~~~~\n$ g++ -O1 -o perhaps-inline other.cpp perhaps-inline.cpp\nperhaps-inline.cpp:1:12: warning: inline function \xe2\x80\x98int InlineFun()\xe2\x80\x99 used but never defined\n    1 | inline int InlineFun();\n      |            ^~~~~~~~~\n/usr/lib/gcc/x86_64-pc-msys/13.2.0/../../../../x86_64-pc-msys/bin/ld: /tmp/cc7eC9Lr.o:perhaps-inline:(.text+0xa): undefined reference to `InlineFun()\'\ncollect2: error: ld returned 1 exit status\n\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们将检查“已达到”的措辞。让我们以不同的方式调用 g++!我们让 g++ 从标准输入读取文件。你瞧,它现在可以“达到”定义了!不再有任何警告。实际上,这会将源文件合并到一个翻译单元中。

\n
$ rm perhaps-inline\n$ cat other.cpp perhaps-inline.cpp | g++ -Wall -O1 -xc++ -o perhaps-inline -\n$ ./perhaps-inline\n$ echo $?\n3\n
Run Code Online (Sandbox Code Playgroud)\n

但是我们可以在 Visual Studio 中观察到类似的效果:对于发布模式(优化打开),链接失败,但如果我们打开整个程序优化,则链接成功:如果构建系统一次查看所有源文件,则“翻译单元”概念变为模糊并且其他文件中的内联函数可用于内联。

\n

底线:

\n
    \n
  • 如果源文件是单独编译的(从而形成单独的翻译单元),这样的程序是格式错误的。
  • \n
  • 编译器履行其职责并以警告的形式发出诊断消息。
  • \n
  • 在没有内联定义的情况下,两个编译器都会尝试保持稳健并回退到生成常规函数调用,从而可能解决链接时缺少的函数定义。如果这样的函数可用(因为它在某处定义为非内联),则此操作会成功。
  • \n
  • 在调试模式下,编译器仍然可以将函数视为非内联的,即发出函数的代码和符号。(这将允许用户在调试会话期间进入它。)这将使链接阶段成功并屏蔽错误(如果忽略警告)。当优化打开时,这样的构建将会失败。
  • \n
  • 构建系统可以自由地将标准的“可达”要求解释为“也许我可以在某个地方找到它,请稍等一下”,例如当整个程序优化打开时。
  • \n
\n