欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Effective C++】彻底了解 inline 函数

程序员文章站 2024-03-23 14:12:16
...

inline 函数有着众多优点:

  • 看起来像是函数,使用起来也像是函数,但是又没有函数调用的额外开销
  • 比宏好用得多,具体见 这篇文章
  • 方便编译器执行语境相关最优化。因为大部分编译器会对那些“不含函数调”用的代码进行优化。所以当你 inline 某个函数,或许编译器就可以对调用它的函数进行语境相关的最优化。

当然天下没有白吃的午餐,inline 函数也有它自己的缺点:

  • 对 inline 函数的每一次调用,都会用 inline 函数本体进行替换。这样就有可能增加目标代码的大小,甚至会导致额外的换页行为,引起缓存页的失效和指令缓存的 miss,造成运行效率损失。

但是另外一方面,如果 inline 函数本身就很小,编译器对 inline 函数调用所产生的目标码可能比函数调用所产生目标码要小,这个时候,inline 函数才发挥了它的正真的作用,产生较小的目标码和较高的指令高速缓存装置的击中率。

下面对 inline 函数做更加细致的讨论。

隐式与显示声明

注意,inline 只是对编译器提出的一个申请,而并不是一个强制命令。它可以利用 inline 关键字是对函数进行的显示 inline 声明,而定义在 class 内部的函数都将会隐式声明为 inline 函数,如下面这样:

class Person
{
public:
	//...
	int age() const { return theAge; }

private:
	int theAge;
};

这个时候,这个 age() 函数就会被隐式定义为 inline 函数。


既然不是强制命令,所以在 inline 使用不合理的情况下,编译器会拒绝对函数进行 inline,比如说下面这几种情况:

1. 函数太复杂

大部分编译器拒绝将太过复杂的函数 inlining,通常是那些含有循环、递归的函数。

2. 对 virtual 函数的 inlining

virtual 函数意味着只有在运行期间才能够确定具体调用类继承结构中的哪一个函数。而 inline 函数通常被放置在头文件内,在编译的过程中进行 inlining,而为了将一个 inline 函数调用替换为被调用的函数本体,编译器就必须知道 inline 函数的样子。这个时候,编译器当然会拒绝对 virtual 函数的 inlining。

3. 取 inline 函数的地址

如果函数要取某一个 inline 函数的地址,编译器就会为这个 inline 函数生成一个 outline 版本。因为 inline 函数并不是函数,所以并不存在函数地址这一说法。比如说下面这种:

typedef void(*pf)();
inline void func() { ... }
pf f = func;
func();
f();

func() 函数调用将会被 inlining,而 f() 或许就不会被 inlining,因为它是通过函数指针进行调用的。


template 和 inline

inline 函数通常被放置在头文件中,而 template 模板函数也通常被放置在头文件中,因为它一旦被使用,编译器需要将模板实例化。所以在很多时候,我们会发现,inline 函数和 template 函数出现在同一函数,比如 std::max:

template<typename T>
inline const T& std::max(const T& a, const T& b)
{
return a < b ? b : a;
}

这个时候,我们可能会有个一错觉,template function 必须是 inline 函数。然而事实并不是,template 模板函数和 inline 函数没有关系。滥用 inline ,可能会招致不必要的麻烦。


是否值得 inlining


1. 构造函数和析构函数不适合 inline

比如说现在有下面这种类继承结构:

class Base
{
private:
	string bm1, bm2;

public:
	//...
};

class Derived : public Base
{
private:
	string dm1,dm2, dm3;

public:
	Derived() { }
	//...
};

由于 Derived() 函数看起来并没有做任何事情,你可能就会将它作为一个 inline 函数。然而实际情况并不是这样。C++ 对于 “对象被创建或者销毁时应该发生什么事情” 有各种保证。当创建对象的时候,对象的 base class 以及每一个成员变量都会被自动构造;当销毁对象的时候,每一个成员变量以及 base class 会被自动析构。如果对象在创建的过程中,发生了异常,那么对象 中已经构造好的部分会自动销毁。在这种情况下,C++ 描述了什么会发生,但是没有说如何发生。“事情如何发生” 是编译器的职责,但是它们一定不会凭空发生,而是编译器会为我们生成一些额外的代码,而这些额外的代码就有可能存在于构造函数和析构函数中。

如果此时 Base 的构造函数也被 inline,那么 Derived 构造函数中 Base 构造函数的调用就会被插入代码,导致 Derive 构造函数代码量增加。

2. inline 函数无法随程序库的升级而升级

如果说,函数 func() 是程序库中的一个 inline 函数,供客户使用。一旦程序库设计者决定改变 func() 函数的实现,那个所有使用到 func() 函数的客户端程序都必须重新进行编译。因为在前面也说过,inline 函数通常是在编译阶段进行替换的。
然而,如果函数 func() 并不是 inline 函数,它有任何的修改,客户端只需要重新链接就好。如果程序库采取的是动态链接,客户端甚至不需要做任何修改。

3. 无法对 inline 函数进行调试

对一个并不存在的函数,大多数编译器是不能够对这种函数设置断点调试的。

.

相关标签: Effective C++