inline,一个被聊烂了的特性

科技   2024-10-09 08:49   浙江  

最近在集中面试,因为招的是cpp开发人员,所以对语言这块的面试在所难免,尤其是对于新特性这块。可能有的人会问,我使用C++03还不是一样可以完成工作,为什么要了解新特性?

其实,项目用C++03完全可以胜任,之所以问新特性,是因为作为一个技术开发人员,要时刻了解技术最前沿,哪怕不用研究的太底层,基础的功能也还是要知道,可惜的是,面了好多人,也就那么一两个通过了技术面试,虽然最终因为种种原因,没能加入😭。

当然了,inline也是我经常问的一个问题,往往以聊聊你对inline的看法为题,开始展开~

基本上所有人都会围绕着优化函数调用来回答,即当编译器遇到一个 inline 函数调用时,它会尝试用函数体的代码替换函数调用的地方。这意味着函数调用的开销(例如堆栈操作和跳转)被消除,代码执行可能会变得更高效。这个回答没错,但却不是我心里想要的答案,于是我会接着问:然后呢?

嗯,然后候选人就卡壳了,这个问题最终结束~

在某次群里聊天的时候,将这个问题在群里抛出来了,然后大家的回答仍然是跟很多候选人回答的一样,虽然我在一年半以前在公众号发过这篇文章inline: 我的理解还停留在20年前,甚至通子出来的某个佬也不清楚~

其实,候选人对inline的回答并没有错,编译器用来优化,但是对于现在的编译器来说,能否优化或者是否会进行优化,已经完全不依赖我们代码中的inline关键字来告诉编译器了(虽然编译器不一定听),编译器有自己的优化策略。

其实,inline还有另外一个Modern赋予其的非常重要的作用,在编译和链接阶段,尤其是针对ODR方面,这也就是作为面试官,最想听到答案~

ODR

在C++中,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.

这一规则是C++语言的核心原则之一,它规定了在同一个程序的不同翻译单元(Translation Units)中,任何命名实体(如变量、函数、类型别名、类模板实例等)都只能有一个定义(Definition),可以有多个声明(Declaration)。换句话说,对于全局作用域的同一实体,其定义在整个程序中只能出现一次,但可以被不同翻译单元通过声明来引用。

违反ODR可能导致未定义行为(Undefined Behavior),例如链接错误或者运行时错误。

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

error: redefinition of 'xxx'

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

在我们开发项目的过程中,有一个不成文的规定,即声明和实现分离(模板除外),其实声明和实现分离的另外一个原因是为了遵守ODR原则

这就往往使得我们像如下这样实现代码:

// fun.h

int fun() {
  return 0;
}

// test.h

int test();

// test.cc

#include "fun.h"

int test() {
  return fun();
}

// test1.h

int test();

// test1.cc

#include "fun.h"

int test1() {
  return fun();
}

// main.cc

#include "fun.h"

int main() {
  return 0;
}

编译命令如下:

 g++ -g test1.cc test.cc main.cc -I . -o odr

报错如下:

in function `fun()':
/test/odr/fun.h:1: multiple definition of `fun()'

/tmp/cc5YLpsp.o:/test/odr/fun.h:1: first defined here
collect2: error: ld returned 1 exit status

上述报错即违反了ODR原则,即:

fun.h中定义了函数fun()test.cc和test1.cc包含了头文件fun.h,利用头文件展开的原理,在编译器生成的test.o和test1.o中会分别存在一份fun()定义main.cc中包含了头文件test.h和test1.h在main.cc中分别调用test()和test1()链接器在链接test.o和test1.o时候,发现fun()有两个定义,不确定要使用哪个,因此就报了前面的错误

看了上面的报错,我们第一时间会考虑如何解决它,后面几节中,将列举几种修改方式,最后从编译的角度去分析这几种方式的区别。

TU

既然在前面一节中已经知道了报错原因,那么就从根上解决这个问题,即重新生成一个文件fun.cc,将fun()函数放入其中,而fun.h则只放fun()函数的声明

// fun.h

// fun.h

int fun();

// fun.cc

#include "fun.h"
int fun() {
  return 0;
}

嗯,编译&链接正常,因为fun()定义只存在于fun.cc中,所以不存在重复定义的问题,这也就解决了一开始的报错问题~

static

另外一种解决方案是通知链接器,我知道有两个定义,但是我只使用自己编译单元中存在的那个,因此可以在该函数前加上static即可:

// fun.h

static int fun() {
  return 0;
}

在编译完成后,每个编译单元有一个fun()定义,但是因为其被声明为static,因此不会被export,因此就避免了一开始的报错。

Modern inline

如果把之前编译器优化的inline称之为传统inline的话,那么C++17起,对inline赋予的新角色,我暂且称之为Modern inline

自C++17起,使用inline,允许在多个TU中存在相同的定义而不违背链接器的ODR规则,虽然某个符号出现在多个TU中,但是最终只会有一个被链接,当然了链接哪个,这个依赖于编译器或者编译器链接TU的顺序~

如果使用inline修改前面的例子,如下:

// fun.h

inline int fun() { // 此处加inline关键字
  return 0;
}

编译&运行,成功~

虽然会有多个fun()符号,但是最终链接器只会选择其中一个,这样同样避免了一开始的报错。

PS:模板函数默认情况下也是内联的,但完整的模板特化则不是,也就是说特化模板如果在多个翻译单元中,将因为违反ODR原则,而导致报错

区别

好了,在前面内容中,使用了3中方式来解决链接过程中的ODR问题,那么问题来了,这几种有什么区别?建议用哪个呢?

当然了,单纯从源码角度看区别,这种只能看个表面,所以,还是建议你从编译的角度来分析,会理解的更为透彻。

使用g++ -c生成.o文件,然后通过nm查看其内容。


test.otest1.o
origin error0000000000000000 T fun0000000000000000 T fun
TUU funU fun
static0000000000000000 t fun0000000000000000 t fun
inline0000000000000000 W fun0000000000000000 W fun

emm,下面是关于上面表格中fun前面的字符说明:

T这个符号表示一个全局或静态函数,其定义在代码段中。也就是说,符号 T 表示这是一个在目标文件中定义的可执行代码部分
U这个符号表示一个未定义的外部符号。也就是说,目标文件中引用了这个符号,但它的定义位于其他文件或目标文件中,通常在链接阶段解析
t这个符号表示一个局部符号,在代码段中定义。也就是说,符号 t 表示这是一个仅在目标文件内部有效的局部函数(或局部变量),不会被其他文件引用
W这个符号表示一个弱符号。弱符号在链接时如果有其他强符号定义,弱符号会被忽略。弱符号通常用于提供默认实现,但允许在其他文件中进行覆盖

static和inline解决方式的区别在于它们处理符号重复定义的方式不同。

嗯,此时,可能有人要问如何选用上面哪种解决方案?答案是具体问题具体分析,使用TU的方式是最差的一种,不过,一般来说建议使用inline,如果你的编译器支持c++17及以上的话~

inline 变量

C++17 引入了inline变量,其语义与inline函数类似,只是它适用于变量。

最常见的是用于类内static变量的初始化:

class Test {
 private:
     inline static int value_ = 0// 等同于类外执行 int Test::value = 0;
};

与inline function一样,inline variable也允许在多个编译单元对同一个变量进行定义,并且在链接时只保留其中的一份作为该变量的定义。当然,同时在多个源文件中定义同一个inline变量必须保证它们的定义都相同,否则和inline函数一样,你没办法保证链接器最终采用的是哪个定义。

inline variable除了支持类内静态成员初始化外,也支持头文件中定义全局变量,这样不会违反ODR规则。

注意

需要注意的是,由于inline关键字有效地忽略了链接器对ODR的检查,这允许它在所有定义的编译单元中选择该符号的任何一个定义,而忽略其他定义。

// test.cc

inline int fun() {
    return 0;
}

int call() {
    return Foo();
}

// main.cc

#include <iostream>

int call();

inline int fun() {
return1;
}

int main() {
    std::cout <<call()<< std::endl;
    std::cout <<fun()<< std::endl;
return0;
}

嗯,上面这个代码可能输出的结果不一样:

g++ main.cc test.cc -o test
// 输出 1 1
g++ test.cc main.cc -o test
// 输出 0 0

也就是说输出结果依赖于编译文件的顺序,emm~

好了,本文内容到此结束,希望你下次再遇到类似问题时能够参考这些信息,找到解决方案。

推荐阅读  点击标题可跳转

1、比“千千静听”还老的装机必备播放器——Winamp公开源代码,但“白嫖”条款惹恼众人

2、C++发布革命性提案:“借鉴”Rust精华,内存安全即将成为标配?

3、嵌入式 C 语言,那些“花里胡哨”的语法特性。

CPP开发者
我们在 Github 维护着 9000+ star 的C语言/C++开发资源。日常分享 C语言 和 C++ 开发相关技术文章,每篇文章都经过精心筛选,一篇文章讲透一个知识点,让读者读有所获~
 最新文章