Item 30:理解 inline 函数的里里外外

inline 函数

inline 函数的优缺点

内联函数的好处太多了:

  • 它没有宏的那些缺点,而且不需要付出函数调用的代价。
  • 同时也方便了编译器基于上下文的优化。

但 inline 函数也并非免费的午餐:

  • 在有限内存的机器上,过分热衷于 inline 化会使得程序对于可用空间来说过于庞大。即使使用了虚拟内存,inline 引起的代码膨胀也会导致附加的分页调度,减少指令缓存命中率,以及随之而来的性能损失。

inline 函数声明方式

inline 只是对编译器的一个请求而非命令。该请求可以隐式地进行也可以显式地声明。

当你的函数较复杂(比如有循环、递归),或者是虚函数时,编译器很可能会拒绝把它 inline。因为虚函数调用只有运行时才能决定调用哪个,而 inline 是在编译器便要嵌入函数体。 有些编译器在 dianotics 级别编译时,会对拒绝 inline 给出 warning 。

隐式的办法便是把函数定义放在类的定义中:

class Person{
    ...
    int age() const{ return _age;}  // 这会生成一个inline函数!
};

例子中是成员函数,如果是友元函数也是一样的。除非友元函数定义在类的外面。

显式的声明则是使用 inline 限定符:

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

模板与 inline

可能你也注意到了 inline 函数和模板一般都定义在头文件中。这是因为 inline 操作是在编译时进行的,而模板的实例化也是编译时进行的。 所以编译器时便需要知道它们的定义。

在绝大多数C++环境中,inline 都发生在编译期。有些环境下也可以在链接时进行 inline,尤其在.NET中可以运行时进行 inline。

但模板实例化和 inline 是两个过程,如果你的函数需要做成 inline 的就把它声明为 inline(也可以隐式地),否则仍然把它声明为正常的函数。

取函数地址

有些适合 inline 的函数编译器仍然不能把它inline,比如你要取一个函数的地址时:

inline void f(){}
void (*pf)() = f;
 
f();        // 这个调用将会被inline,它是个普通的函数调用
pf();       // 这个是通过指针调用的,不会被inline

构造/析构函数

构造析构函数看起来很适合 inline,但事实并非如此。我们知道C++会在对象创建和销毁时保证做很多事情,比如调用new时会导致构造函数被调用, 退出作用域时析构函数被调用,构造函数调用前成员对象的构造函数被调用,构造失败后成员对象被析构等等。

这些事情不是平白无故发生的,编译器会生成一些代码并在编译时插入你的程序。比如编译后一个类的构造过程可能是这样的:

Derived::Derived(){
    Base::Base();
    try{ data1.std::string::string(); }
    catch(...){
        Base::Base();
        throw;
    }
    try{ data2.std::string::string(); }
    catch(...){
        data1.std::string::~string();
        Base::~Base();
        throw;
    }
    ...
}

Derived 的析构函数、Base 的构造和析构函数也是一样的,事实上构造和析构函数会被大量地调用。 如果全部inline 的话,这些调用都会被扩展为函数体,势必会造成目标代码膨胀。

如果你是库的设计者,那么你的接口函数的 inline 特性的变化将会导致客户代码的重新编译。 因为如果你的接口是 inline 的,那么客户需要将函数体展开编译到客户的目标代码中。

总结

  • inline 只是对编译器的一个请求而非命令,编译器可能忽略这一请求。
  • 定义在类中的成员函数和友元函数默认 inline。
  • 将大部分 inline 限制在小的,调用频繁的函数上。这使得程序调试和二进制升级更加容易,最小化潜在的代码膨胀,并最大化提高程序速度的几率。
  • 不要仅仅因为函数模板出现在头文件中,就将它声明为 inline。
posted @ 2020-02-12 21:37  刘-皇叔  阅读(210)  评论(0编辑  收藏  举报