您的位置:首页 > 编程语言 > C语言/C++

C++那些细节--inline关键字

2015-09-20 19:23 411 查看

一.简介

inline是个好东西,不过要注意不能乱用。在项目中看到过许多inline相关的宏定义,_forceinline,_inline等等,有许多有疑惑的地方。于是,本人强迫症发作,决定总结一下inline相关的知识。主要涉及到inline的功能,使用,以及forceinline等。还有类中的virtual函数是否会被inline等问题。

二.inline关键字

1.inline优点

inline是标准C++中提供的关键字,使用这个关键字,意思就是告诉编译器,介个函数是个内联函数。内联函数又是啥呢,简单的说就是直接将函数的内容替换到调用的位置。这样做,可以大大的省掉调用函数的性能损失。内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。内联函数在使用时,没有函数调用的性能损失。但是却可以像函数一样,有相关的类型检查等,我们完全可以按照普通函数那样对待内联函数。

看一个例子,比如我们的类中有一个成员变量,我们在使用这个成员变量的时候,为了封装性,需要使用函数来存取这个变量。
class Test
{
private:
	int m_iID;
public:
	void SetID(int id);
	int GetID();
};

void Test::SetID(int id)
{
	m_iID = id;
}

int Test::GetID()
{
	return m_iID;
}

但是,我们这样做的话,本来一下子就可以搞定的事情,却需要通过函数来实现。虽然这样有利于封装,为了更严谨的结构,我们写的时候麻烦一些也就罢了。但是,在运行时,这样做会造成很大的调用函数的损失。如果调用成千上万次的话,每次调用都需要进行参数压栈,保存运行状态,将结果返回等一系列操作,而我们的函数这么短小,使用普通函数就有些得不偿失了,这时候,把它内联了可能是最好的选择。内联之后,我们就可以在编程的时候将它按照函数处理,而在使用的时候又没有多少性能开销,何乐而不为呢?

2.inline缺点

既然inline这么好,那么我们是不是可以任意使用inline函数呢?
凡事有一利必有一弊,inline虽然能够很好的提升性能,但是,它的实现是通过将每一个函数的调用都以函数本身来替代进行实现的。这样做会有一些弊端。
(1)程序体积增大:函数调用次数越多,我们插入的内容就越多,代码体积势必会增大。程序体积增大的话,会导致额外的虚拟内存换页等行为,降低指令高速缓存的命中率。这样的话,inline反倒降低了效率。
(2)不利于动态升级。inline函数一般都在.h文件中,将声明和定义写在一起,普通函数修改函数内部内容时只需要重新连接就可以,但是内联函数如果修改的话,所有用到该头文件的内容都需要重新编译。
(3)不利于调试,内联函数的实现是拷贝过去的话,那么真正调用的时候,那里根本就不是一个函数,我们没有办法去调试一个内联函数。所以,编译器在编译debug版本的时候,是不会将内联函数inline处理的,而是当做一个普通的函数来处理,这样我们就可以调试了。

3.怎样进行inline

inline这么好,那么肿么进行inline操作呢?inline有两种形式,一种是显式inline,另一种是隐式inline。
显式inline顾名思义,就是直接使用inline关键字,指定一个函数是inline的。这种情况inline一般是放在类的外面,函数声明中没有写inline关键字,在函数定义中添加inline关键字。比如:
class Test
{
private:
	int m_iID;
public:
	void SetID(int id);
	int GetID();
};

inline void Test::SetID(int id)
{
	m_iID = id;
}

inline int Test::GetID()
{
	return m_iID;
}
不过呢,这种inline貌似不是太常用。一般都是使用隐式inline的。当我们把函数的定义放在类的内部时,这个函数就会被隐式inline,我们不需要再写inline关键字了。
class Test
{
private:
	int m_iID;
public:
	//函数的定义在类内部的,隐式inline
	void SetID(int id)
	{
		m_iID = id;
	}
	int GetID()
	{
		return m_iID;
	}
};
一般inline函数都是放在.h文件中的,函数的声明和定义放在一起。
如果我们在类中将一个函数的定义放在了类的内部,那么这个函数就会被声明为inline。当然,这一条对构造函数和析构函数也是有效的。不过,构造函数和析构函数最好不要inline的,会出现比较麻烦的情况。

4.一定会inline吗?

答案是不一定。inline关键字跟其他的关键字不太一样,他仅仅是一个请求,执不执行不一定。最终一个函数会不会被inline还是看编译器的脾气。我们写程序的时候,在函数前面加上inline关键字或者直接将函数定义在类中之后,编译时,编译器会进行相关的分析,看一下这个函数值不值得内联,如果不值得,它会忽略我们的inline请求。
inline函数一般对应的函数是那种体积比较小或者经常调用的函数。而这种函数的特点就是短小,函数本身的操作还没有函数调用性能损失大。所以,对于下面两种函数,编译器是不会进行内联的:
(1)过长的函数。比如一个1000多行的函数,编译器根本不会理会inline的请求的。如果这个inline了,那么所有调用该函数的地方都插入这1000多行的程序,那么程序的体积会膨大很多。
(2)包含循环或者递归的函数。
经过了这个检查,编译器会决定是否对函数进行inline操作。但是,这也不是绝对的,还有7种情况,编译器仍然不会对我们的函数进行inline操作,即使是使用我们之后介绍的forceinline关键字,仍然不能进行inline。

三.__inline与__forceinline关键字

关于__inline与__forceinline关键字的话,之前在项目中看到过,比较纠结它们与inline的区别与联系,今天也顺便整理一下。

1.__inline关键字

__inline关键字比较简单,它是Microsoft的编译器中提供的一个关键字,可以用于C和C++但是仅限于微软的编译器,而Inline是标准C++提供的关键字,仅能用于C++,但是不限定编译器。
而__inlined的其他情况都与inline关键字相同,所以这里不再赘述。

2.__forceinline关键字

__forceinline关键字也是Microsoft的编译器中提供的一个关键字,可以用于C和C++但是仅限于微软的编译器。
这个关键字看起来很牛,字面上翻译就是强制内联。



3.__forceinline真的能强制内联吗

虽然它叫强制内联,然而能不能内联还是要看情况滴!!显然,它也是一个请求,最终还是编译器决定能不能内联。下面看一下,哪几种情况是不能内联的(注意,__forceinline都不能强制内联的,inline当然就更不能内联了):
Even with __forceinline, the compiler cannot inline code in all circumstances. The compiler cannot inline a function if:

(1)The function or its caller is compiled with /Ob0 (the default option for debug builds).

(2)The function and the caller use different types of exception handling (C++ exception handling in one, structured exception handling in the other).

(3)The function has a variable argument list.

(4)The function uses inline assembly, unless compiled with /Og, /Ox, /O1, or /O2.

(5)The function is recursive and not accompanied by #pragma inline_recursion(on). With the pragma, recursive functions are inlined to a default depth of 16 calls. To reduce the inlining depth, use inline_depth pragma.

(6)The function is virtual and is called virtually. Direct calls to virtual functions can be inlined.

(7)The program takes the address of the function and the call is made via the pointer to the function. Direct calls to functions that have had their address taken can be inlined.

(8)The function is also marked with the naked __declspec modifier.
上面的内容来自微软的MSDN,翻译一下:
(1)函数或其调用者使用/Ob0编译器选项进行编译(Debug模式下的默认选项)。也就是说在Debug模式下,是不会发生函数内联的。
(2)函数和其调用者使用不同类型的异常处理。
(3)函数具有可变数目的参数。
(4)函数使用了在线汇编(即直接在你C/C++代码里加入汇编语言代码)。但使用了编译器关于优化的选项/Og,/Ox,/O1,或/O2的情况除外。
(5)函数是递归的并且不伴有#inline_recursion(on)。递归函数内联调用默认的深度为16。为了减少内联深度,使用inline_depth。
(6).是虚函数并且是虚调用。但对虚函数的直接调用可以inline。
(7)通过指向该函数的函数指针进行调用。
(8)函数被关键字__declspec(naked)修饰。

好吧,看来想要内联一个函数还是挺难的,有这么多要求。不过,一般的话我们只要记住这几个关键点:第一,函数短小精悍,函数本身的开销比调用函数的开销小。第二,不要有过长的代码,不要有循环,递归。第三,有virtual时不会有Inline。第四,通过函数指针调用函数的时候不会有内联。第五,Debug下没有内联。

四.关于inline的一些细节问题

好吧,本人强迫症发作,决定刨根问底一下,再总结一下关于inline的几个特殊的地方。

1.inline和virtual

首先,他俩是冲突的,但是并不会报错。因为inline只是一个请求,在同时有virtual和inline的时候,编译器会首先满足virtual,而忽略我们的inline请求。其实在类中virtual函数和inline同时出现的时候还是挺多的,虽然我们可能并没有写inline(因为隐式Inline)。
多态的实现是由虚表加以支持的,凡是有虚函数的对象,都会在构造函数开始时构造一个虚表,虚表中的第一个元素一般是对象的类型信息,其他每个元素存放的是真正函数的地址,如果子类覆盖了父类的虚函数,则对应的位置中的地址就会被修改,但是同一个函数在虚表中的位置即下标是相同的。当我们用基类指针或者引用调用一个虚函数时,在编译期只知道该函数在某个虚表的第几个位置,但是不知道是父类的虚表还是子类的虚表,只有到运行时才能确定是哪一个虚表,从而表现出多态。但如果你不是使用基类的指针或者引用调用虚函数,或者你调用的不是虚函数,则在编译期间就可以直接找到成员函数的地址,不需要等到运行时才确定,因为此时,调用者是哪个对象已经确定,从而该函数的地址也是确定的。

虽然virtual所代表的多态类型是要在运行时确定的,但是如果调用者不是基类的指针或者引用,则该virtual的地址会在编译期间就确定,因而此时可以用inline进行展开。即使使用了基类的指针或引用进行调用,也不会产生错误,此时inline将不会展开,但virtual仍然表现出多态,因为inline毕竟只是建议,而不是强制,所以两者不矛盾。
其实简单分析一下,就应该明白,两者的确是冲突的。因为inline的机制是在编译时就进行了函数的替换和展开。函数要调用什么,这是在运行之前就决定了的。而virtual使用的是动态绑定,简单来说就是根据运行时的动态类型,去虚函数表中查找对应的函数。所以,Inline那套编译时替换的行为肯定是不会有多态的!!显然,当virtual和inline冲突的时候,编译器一定会为了正确而牺牲掉性能的。仅有我们不使用基类的指针或者引用来调用virtual函数时,Inline才会展开。

2.构造函数&析构函数不要inline

构造函数和析构函数能不能inline?答案是能。只要把函数定义在类里面,就inline了,甭管是构造还是析构,甚至是friend。
但是构造函数和析构函数最好不要进行inline。这是《Effective C++》中的建议,不过还是来看看为什么。
构造函数中可能有很多异常处理相关的东西,虽然我们看不到,不过这东西编译器给加的。所以即使我们的构造函数看起来空空如也,里面也不是真正的什么都没有!!加入构造函数inline了,初始化列表中的对象也是inline的话,那么,展开之后,这个构造函数就会庞大无比。
析构函数也是如此。而且经常有徐析构函数,这个本身也是和inlne冲突的。

所以简单粗暴的记住就好,而且VS自动生成的类也是把构造函数和析构函数分开到.cpp文件中的。

3.inline和宏的比较

没有inline之前,貌似我们经常这样定义一个简单的类似函数的宏:
#define MAX(x, y) ((x) > (y) ? (x) : (y))

int _tmain(int argc, _TCHAR* argv[])
{
	int a = 3;
	int b = 4;
	cout<<MAX(a, b)<<endl;

	system("pause");
	return 0;
}
通过这样的宏定义,达到简单函数替换的效果。不过,这种替换跟inline相比简直就是小巫见大巫了。首先#define就是简单的替换,没有什么诸如安全类型检查,自动类型转化等等。而且这种替换也存在着一些风险,使用宏定义的时候要慎重得多才行。而使用inline,我们完全可以像普通函数那样操作Inline函数,有类型检查,自动类型转化,而且还可以像成员函数那样,妥善的处理this指针。并且,使用inline的话,是编译器为我们进行更加深入的优化,这也是宏定义做不到的。编译器还会为我们分析,是不是值得inline,如果不值得,就不会inline。

4.Debug下不inline

因为inline会导致函数被拷贝到调用的地方,所以实际上Inline函数并不再是一个函数。因而绝大多数的调试器都对其束手无策。所以,在调试的时候,编译器选择不进行内联处理。即,debug模式下是没有进行内联的。这样,我们就可以像调试普通函数那样调试“内联函数”了。

5.在使用函数指针调用时不Inline

虽然函数是inline函数,但是,有时候我们如果使用函数指针调用这个函数的话,那么就不会进行inline处理,编译器会生成一个真正的函数实体,来达到正常的函数调用过程。所以这种情况下,肯定是实体函数,函数不会被Inline。

五.总结

inline是一个很好的东东,但是我们要慎用。虽然Inline可以去掉函数调用时的损失,但是这是以拷贝替换为代价的。而且将实现和声明放在一起(不放在一起的话会导致一个inline多个实现),会导致inline函数无法随着动态程序库升级,如果改动inline函数,会导致所有使用该inline函数的文件被重新编译。

正如《Effective C++》中所说,一开始不要将函数置为inline,除了那些一定为inline或者平淡无奇的函数(比如SetValue,GetValue等)。记住80-20法则,程序80%的运行时间花费在20%的代码上。所以,我们要找出这能够增进程序效率的20%的代码,竭尽所能的为其瘦身以致将其inline。

参考资料:
http://blog.csdn.net/imyfriend/article/details/12676229

http://m.blog.csdn.net/blog/zjb204/19829779

http://www.cnblogs.com/berry/articles/1582702.html

http://blog.chinaunix.net/uid-26548237-id-3786623.html

《Effective C++》条款30:透彻了解inline的里里外外
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: