inline: 我的理解还停留在20年前

2023-06-13 14:48:19 浏览数 (1)

你好,我是雨乐~

在上篇文章访问私有变量——从技术实现的角度破坏"封装"性一文中,在第二个实现示例中,用到了inline 变量,一开始,是懵逼的,因为在我的印象中inline 仅仅函数,而在此处却用于声明变量。于是,赶紧去查阅资料,发现自CPP17开始,引入了inline 变量,这个时候突然不是那么自责了,毕竟我的cpp知识积累止步于cpp11。不过,为了研究那段代码,还是仔细研究了下,不看不要紧,一看吓一跳,原来我对inline的理解停留在n年前。于是赶紧恶补这方面的知识,而这篇文章呢,就是我最近研究的一个知识点总结。

狭隘的理解

inline源于C,与关键字register作用一样,旨在让编译器进行优化,inline的思想源自于C的预处理宏,而后者又源自汇编语言。其目标是省略因为函数调用而引起的部分开销。与预处理宏不一样的是,inline支持类型检查,而这就是inline引入C 的初衷(旨在具有宏的功能,且支持类型检查)。

在编译过程中,编译器维护了一组数据结构,称之为**符号表(Symbol Table)**。对于普通函数,编译器只把函数名称(对于c 来说,需要经过name mangling,毕竟运行函数重载,而C则不需要)和返回值记录在符号表里。而对于inline函数(编译器确认可以inline的),除上述的函数名称和返回值之外,也将函数的实现(究竟存放源代码还是编译后的汇编指令就看编译器的实现了)放在符号表中。当遇到内联函数的调用时,编译器首先检查调用是否正确(参数类型检查,返回结果是否被正确使用——对于普通函数也进行这些检查),检查无误后将内联函数的函数体替换掉对它的调用,从而省去调用函数的开销(参数入栈,汇编CALL等),这就是inline后性能优于普通函数调用的原因。

当然了,编译器是否决定inline,有它自己的规则,代码中指定inline关键字也只是建议编译器内联,最终是否真正inline取决于具体场景。

以上,就是我对inline的理解,也就是说在之前,我的错误理解是inline作用仅限于inline function,即编译时进行指令替换

概念

在阅读本文后面的章节之前,需要先了解两个概念ADLODR

ADL

ADL是Argument Dependent Lookup的缩写,又称为Koenig Lookup(最开始以发明人的名称进行命名),一般译为参数依赖查找,是用于在函数调用表达式中查找非限定函数名称的规则集。

可以理解为如果在使用函数的上下文中找不到函数定义,我们可以在其参数的名字空间中查找该函数的定义。

c 11标准对其定义如下:

When the postfix-expression in a function call (5.2.2) is an unqualified-id, other namespaces not considered during the usual unqualified lookup (3.4.1) may be searched, and in those namespaces, namespace-scope friend function declarations (11.3) not otherwise visible may be found. These modifications to the search depend on the types of the arguments (and for template template arguments, the namespace of the template argument).

这种方式其实我们经常用到,比如,在上篇文章访问私有成员——从技术实现的角度破坏"封装" 性友元函数那一块已经用到了(在类内进行函数定义(参数为类类型),类外无序声明可以直接调用),只是没有留意罢了~~

通过个例子来简单理解下,该例子来源于stackoverflow:

代码语言:javascript复制
namespace MyNamespace {
    class MyClass {};
    void doSomething(MyClass) {}
}

MyNamespace::MyClass obj; // global object

int main() {
    doSomething(obj); // Works Fine - MyNamespace::doSomething() is called.
}

如上例,doSomething()首先在其上下文中查找定义(namespace的除外),没有找到,然后依赖了ADL规则,在其参数obj所在范围(MyNamespace)内找到了定义,所以编译正常。

ODR

ODR是One definition Rule的缩写,中文称之为单一定义规则

cppreference中的定义如下:

Only one definition of any variable, function, class type, enumeration type, concept (since C 20) or template is allowed in any one translation unit (some of these may have multiple declarations, but only one definition is allowed). One and only one definition of every non-inline function or variable that is odr-used (see below) is required to appear in the entire program (including any standard and user-defined libraries). The compiler is not required to diagnose this violation, but the behavior of the program that violates it is undefined.

从上述定义,可以看出,对于声明为非inline的函数或者变量,在整个程序里只允许有一个定义。而如果有多个的话,则会破坏ODR原则,在链接阶段因为多个符号冲突而失败。

C 程序通常由多个C 源文件组成(.cc/.cpp等),编译器在进行编译的时候,通常是将这些文件单独编译成模块或者目标文件,然后通过链接器将所有模块/目标文件链接到一个可执行文件或共享/静态库中。

在链接阶段,如果链接器可以找到多个同一个符号的定义,则认为是错误的,因为其不知道使用哪个,这个时候,就会出现链接器报错,如下这种:

代码语言:javascript复制
error: redefinition of 'xxx'

而这个报错原因,就是因为没有遵循ODR原则,下图易于理解:

也就是说,函数或者变量在整个程序中只能定义一次(全局,非namespace 非inline等),而这种规则,往往使得我们在编码的时候,将声明放到某个头文件,比如header.h,而将定义放在header.cc。但是,往往在多人协作项目中,这种很难满足,比如对于函数名相同,参数相同,而实现不同,对于这种如果不采取其他方式的话,往往就会破坏ODR原则,导致链接失败。对于这种情况,往往使用static定义、namespace以及本文要讲的inline

inline function

下面看下inline function的定义:

An inline function is one for which the compiler copies the code from the function definition directly into the code of the calling function rather than creating a separate set of instructions in memory.

从上面的定义可以看出,对于声明为inline的函数,在调用该inline函数的时候,编译器会直接进行代码替换,也就是说省略了函数调用这个步骤。

我们先看一段代码,如下:

代码语言:javascript复制
inline int add(int a, int b){
    return a   b;
}

int main(){
    int x = 3
    int y = 4;
    int z = add(x, y);
    // do sth
    return 0;
}

编译器会将上述代码优化为:

代码语言:javascript复制
int main(){
    int x = 3
    int y = 4;
    int z = x   y; // inline 代码替换
    // do sth
    return 0;
}

当然,上述是从编译器对inline函数处理的角度来理解的,往往编译器会进行更加直接的优化,即优化成int z = 7

以上,可能就是大部分人认为的inline function,即对function 加 inline关键字以建议编译器将该函数进行inline。

但是,建议往往是建议,对于编译器来说,大部分的建议都不会被采纳,它(编译器)总是有自己的理由来决定在什么地方进行inline,什么地方进行函数调用,也就是说,编译器比开发人员更加清楚什么地方应该inline。或者说,大部分人认为的inline function,在理解上是狭隘的,或者说,对于Modern CPP来说,这种理解是错误的,是过时的。

inline 关键字用于函数,有两个作用,第一个作用(相对老版本编译器来说),就是前面说的(指令或者代码替换);而第二个,使得在多个翻译单元(Translation Unit, 在此可以理解为.cc/.cpp等源文件)定义同名同参函数成为了可能。

先看下面的代码:

file1.cc

代码语言:javascript复制
int f() { 
  return 0; 
}

int main() { 
  return f(); 
}

file2.cc

代码语言:javascript复制
int f() { return 0; }

使用如下命令进行编译:

代码语言:javascript复制
gcc file1.cpp file2.cpp

在链接的时候,报错如下:

代码语言:javascript复制
file2.cc:(.text 0x0): multiple definition of `f()'

相信这种报错,大家都遇到过,而且不止一次。这是因为编译器在进行编译的时候,是以(.cc/cpp等)文件为单元进行单独编译成.o文件,然后在链接阶段对这些.o文件进行链接,发现有重复定义,这也就有了上面的报错,这种错误的根本原因就是违反了ODR原则

这个时候,就是inline大显身手的时候。

在定义函数的时候,前面加上inline关键字,就可以避免上面的重复定义错误,这种做法相当于告诉编译器:在编译的时候,遇到这种包含inline关键字的重复定义函数,不用再报错了

0 人点赞