max 是一个模板的事实引出一个观察结论:inline 函数和模板一般都是定义在头文件中的。这就使得一些程序员得出结论断定函数模板必须是 inline。这个结论是非法的而且有潜在的危害,所以它值得我们考察一下。 inline 函数一般必须在头文件内,因为大多数构建环境在编译期间进行 inline 化。为了用被调用函数的函数本体替换一个函数调用,编译器必须知道函数看起来像什么样子。(有一些构建环境可以在连接期间进行 inline 化,还有少数几个——比如,基于 .NET Common Language Infrastructure (CLI) 的控制环境——居然能在运行时 inline 化。然而,这些环境都是例外,并非规则。inline 化在大多数 C++ 程序中是一个编译时行为。)
模板一般在头文件内,因为编译器需要知道一个模板看起来像什么以便用到它时对它进行实例化。(同样,也不是全部如此。一些构建环境可以在连接期间进行模板实例化。然而,编译期实例化更为普遍。) 模板实例化与 inline 化无关。如果你写了一个模板,而且你认为所有从这个模板实例化出来的函数都应该是 inline 的,那么就声明这个模板为 inline,这就是上面的 std::max 的实现被做的事情。但是如果你为没有理由要 inline 化的函数写了一个模板,就要避免声明这个模板为 inline(无论显式的还是隐式的)。inline 化是有成本的,而且你不希望在毫无预见的情况下遭遇它们。我们已经说到 inline 化是如何引起代码膨胀的,但是,还有其它的成本,过一会儿我们再讨论。
在做这件事之前,我们先来完成对这个结论的考察:inline 是一个编译器可能忽略的请求。大多数编译器拒绝它们认为太复杂的 inline 函数(例如,那些包含循环或者递归的),而且,除了最细碎的以外的全部虚拟函数的调用都不会被 inline 化。不应该对这后一个结论感到惊讶。虚拟意味着“等待,直到运行时才能断定哪一个函数被调用”,而 inline 意味着“执行之前,用被调用函数取代调用的地方”。如果编译器不知道哪一个函数将被调用,你很难责备它们拒绝 inline 化这个函数本体。
所有这些加在一起,得出:一个被指定的 inline 函数是否能真的被 inline 化,取决于你所使用的构建环境——主要是编译器。幸运的是,大多数编译器都有一个诊断层次,在它们不能 inline 化一个你提出的函数时,会导致一个警告。
有时候,即使当编译器完全心甘情愿地 inline 化一个函数,他们还是会为这个 inline 函数生成函数本体。例如,如果你的程序要持有一个 inline 函数的地址,编译器必须为它生成一个 outlined 函数本体。他们怎么能生成一个指向根本不存在的函数的指针呢?再加上,编译器一般不会对通过函数指针的调用进行 inline 化,这就意味着,对一个 inline 函数的调用可能被也可能不被 inline 化,依赖于这个调用是如何做成的:
inline void f() {...} // assume compilers are willing to inline calls to f
void (*pf)() = f; // pf points to f ...
f(); // this call will be inlined, because it’s a "normal" call pf(); // this call probably won’t be, because it’s through // a function pointer |
查看本文来源