《Effective C++》学习笔记——条款30

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

五、Implementations

Rule 30:Understand the ins and outs of inlining

规则 30:透彻了解inlining的里里外外

1.inline 的优缺点

> 优点

——看起来像函数

——动作像函数

——比宏好很多

——可以调用它们又不需要蒙受函数调用所招致的额外开销

——编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码,所以当你inline某个函数,或许编译器就因此——有能力对它(函数本体)执行预警相关最优化。大部分的编译器不会对一个“outlined函数调用”动作执行如此之最优化

> 缺点

——可能增加目标码(object code)的大小

——inline造成的代码膨胀会导致额外的换页行为,降低指令告诉缓存装置的击中率,以及伴随这些而来的效率损失

2.观点

记住——
inline只是对编译器的一个申请,不是强制命令。

> 声明

这项申请可以隐喻的提出,也可以明确的提出。

隐喻方式是将函数定义于class定义式内:

class Person  {
public:
    ...
    // 一个隐喻的inline申请:age被定义于class定义式内
    int age() const  {  return theAge;  }
    ...
private:
    int theAge;
};

在条款46中会提到,friend函数也可以定义于class内,所以它们也是被隐喻声明为 inline

明确声明做法则是在其定义式前加上关键字inline。例如标准的max template(来自<algorithm>)往往这样实现:

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

> inline and template

inline函数 和 template 两者通常都被定义在头文件内,这使得某些程序员认为 function template 一定必须是inline,有必要好好看一下这两者:

——Inline函数通常一定被置于头文件内,因为大多数建置环境在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。某些建置环境可以再连接期完成inlining,少量建置环境如 基于.NET CLI;公共语的托管环境竟可在运行期完成inlining。

inlining在大多数C++程序中是编译器行为。

——Template 通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。

Template的具现化与inlining无关。如果你写的template没有理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline。

因为inline需要成本,在缺点中提到过代码膨胀,但还存在其他的成本。

3.于编译器角度看 inline

一个表面上看似inline的函数是否真是inline,取决于你的建置环境,主要取决于编译器。

幸运的一件事——大多数编译器提供了一个诊断级别:如果它们无法将你要求的函数inline化,会给一个warning

> 大部分编译器拒绝将太过复杂(含有循环或递归)的函数inlining,而所有对virtual函数的调用也都会使inline落空。

因为virtual意味着“等待,直到运行期才确定调用哪个函数”,而inline意味着“执行前,先将调用动作替换为被调用函数的本体”。编译器根本不知道你调用哪个函数,如何替换?

> 有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体。

比如,如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个 outlined 函数本体。毕竟编译器无法让一个指针指向并不存在的函数。

而且,编译器通常不对“通过函数指针而进行的调用”实施inlining,这意味对inline函数的调用可能被inlined,取决于该调用的实施方式:

// 假设编译器有意愿inline“对f的调用”
inline void f()  {  ...  }
// pf 指向 f
void (*pf)() = f;
...
// 这个调用将被inlined,因为他是一个正常调用
f();
// 这个调用或许不被inlined,因为它通过函数指针达成
pf();

实际上构造函数和析构函数往往是inlining的糟糕候选人——虽然漫不经心的情况下你不会这么认为。

class Base  {
public:
    ...
private:
    std::string bm1,bm2;
};
class Derived : public Base  {
public:
    Derived()  {  }
    ...
private:
    std::string dm1,dm2,dm3;
};

这个构造函数看起来是inlining的候选人,因为它根本不包含任何代码。

但 事实并不如此 !

C++对于“对象被创建和被销毁时发生什么事”做了各式各样的保证。

当你使用 new,动态创建的对象被其构造函数自动初始化;当你使用delete,对应的析构函数会被调用。

当你创建一个对象,其每一个base class以及每一个成员变量都会被自动构造;当你销毁一个对象,反向程序的析构行为亦会自动发生。

如果有个异常在对象构造期间被抛出,该对象已构造好的那一部分会被自动销毁。

在这些情况中C++描述了什么一定会发生,但没有说如何发生。

“事情如何发生”是编译器实现者的权责,它们不可能凭空发生,在我们的程序中一定有某些代码让那些事情发生。

这些代码有时候放在你的构造函数和析构函数内,所以,我们可以想象到,编译器为上面那个看起来是空的Derived构造函数所产生的代码,应该是这样的:

Derived::Derived()
{
    // 初始化 Base成分
    Base::Base();
    // 试图构造dm1
    try  {  dm1.std::string::string();  }
    catch(...)  {
        // 如果抛出异常  销毁 base class成分,并传播该异常
        Base::~Base();
        throw;
    }
    // 试图构造dm2
    try  {  dm2.std::string::string();  }
    catch(...)  {
        // 如果抛出异常,销毁dm1  base class 成分,并传播异常
        dm1.std::string::~string();
        Base::~Base();
        throw;
    }
    // 试图构造dm3
    try  {  dm3.std::string::string();  }
    catch(...)  {
        // 如果抛出异常,销毁dm2,dm1,base class成分,并传播该异常
        dm2.std::string::~string();
        dm1.std::string::~string();
        Base::~Base();
        throw;
    }
}

这段代码并不能代表编译器真正能制造出来的代码,因为真正的编译器会以更精致复杂的做法来处理异常。

虽然如此,这也已经准确反映Derived的空白构造函数所必须提供的行为。

不论编译器在其内所做的异常处理多么精致复杂,Derived的构造函数至少一定会陆续调用其成员变量和base class两者的构造函数,而那些调用(它们自身也可能被inlined)会影响编译器是否对此空白函数inlining。

相同理由也适用于Base构造函数,所以如果它被inlined,所有替换“Base构造函数调用”而插入的代码也都会被插入到“Derived构造函数调用”内(因为Derived构造函数调用了Base构造函数)。

所以,程序库设计者必须评估“将函数声明为inline”的冲击;inline函数无法随着程序库的升级而升级。

若从纯粹实用的观点出发,有一个事实比其他因素更重要:大部分调试器面对inline函数都束手无策。

毕竟你没办法在一个并不存在的函数内设立断点。

4.总结

在决定哪些函数盖被声明为inline而哪些函数不该时,掌握一个合乎逻辑的策略

> 一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些”一定成为inline”或“十分平淡无奇”的函数身上。

> 慎重使用inline便是对日后使用调试器带来帮助,不过这么一来也等于把自己推向手工最优化的道路。

> 不要忘记 80-20 经验法则:平均一个程序往往将80%的执行时间花在20%的代码上头。所以,找出可以有效增进程序整体效率的20%代码,然后将它inline或接近所能地将它瘦身。BUT,首先你要选对目标!

☆ 请记住 ☆

? 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

? 不要只因为 function templates 出现在头文件,就将它们声明为inline。

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

时间: 2024-08-03 07:09:42

《Effective C++》学习笔记——条款30的相关文章

Effective C++学习笔记 条款07:为多态基类声明virtual析构函数

一.C++明确指出:当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义——实际执行时通常发生的是对象的derived成分没有被销毁!(注:使用基类引用派生类的方式使用多态,由于引用只是对原对象的一个引用或者叫做别名,其并没有分配内存,对其引用对象内存的销毁,将由原对象自己负责,所以使用引用的时候,如果析构函数不为virtual,那么也不会发生derived成员没有销毁的情况) 例如: class b

effective c++学习笔记条款23-25

条款23:宁可用非成员,非友元函数来替代成员函数 1.非成员函数提供了更好的封装性,这个函数内不能访问类的私有成员,封装的越严密我们对类的数据就可以弹性越大的操纵,因为可见这些数据的客户越少,反之数据影响的客户也就越少. 2.c++比较自然的做法-(关系到标准库numplace的组织结构),可以把不同便捷函数放到不同Namespace去,让客户来决定要用的非成员函数功能,这是类不能提供的. 条款24:若所有参数皆需类型转换,请为此采用非成员函数. 1.如果你需要为某个函数的所有参数(包括被thi

effective c++学习笔记条款11-13

条款11: 1.令赋值运算符返回一个&,因为STL,string都是这样做的,除非你有足够好的理由不这样做. 2.处理自我赋值的方法----(1).在没有成功获取对象数据时不要删除自己的数据,避免发生异常后原对象指针是一个悬浮指针 (2).判断自我赋值的检查操作会耗费不少时间,可以用swap交换数据技术来优化---(1)形参为赋值而来,(2)形参为静态引用,多加一个函数内拷贝操作.

effective c++学习笔记条款8-10

条款7:为多态基类声明虚析构函数 1.一个基类指针接受一个派生类对象的地址时,对该指针delete,仅仅释放基类部分 2.给所有类都带上虚析构函数是个馊主意,会带有vptr指向一个函数指针数组,扩大不必要的对象大小,除非补偿vptr,否则没有移植性. 3.string类和STL不含有虚析构函数,然而一些用户 却将他们作为基类,运用   delete指向派生类的基类指针,导致错误[c++11添加了禁止派生性质],他们不适合当基类. 4,手头上没有合适的纯虚函数,但你确实需要一个抽象类,把析构函数声

effective c++学习笔记条款20-22

条款20:用引用传递代替值传递 1.尽量以引用传递来代替传值传递,前者比较高效,并且可以避免切割问题 2.以上规则不适用于内置类型,以及STL的迭代器,和函数对象 条款21:必须返回对象时,别妄想返回对象的引用 1.绝对不要返回指针和引用指向一个局部对象或者静态局部对象而有可能需要多个这样的对象,条款4已经为在单线程环境合理返回&指向一个局部静态提供了一份设计实例.(保护初始化顺序) 条款22:将成员变量声明为private 1.切记将成员变量声明为private.这可赋予客户访问数据的一致性,

effective c++学习笔记条款4-7

条款4:确定对象被使用前已经初始化 一. 变量在不同情况下可能会初始化,也可能不会初始化. 注意初始化和赋值的区别. 1.在类中内置类型不会发生隐式初始化,自定义有默认构造函数的能被默认初始化 所以在构造类时务必初始化内置类型,最好给自定义的对象显示初始化避免在函数体中赋值浪费资源. 2.内置类型在函数体内不会初始化,在函数体外自动初始化为0. 二. 1.const和引用类型必须初始化,不可能赋值 三 1.当类实在是有较多构造函数,并且总是要对一些成员数据重复初始化,可以考虑将那些“赋值和初始化

effective c++学习笔记条款17-19

条款17:以独立语句将New对象放置入智能指针. 1.以独立语句将newed对象放置入智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露. void name(shared_ptr<管理对象类型>(new 管理对象类型),其它函数)),New被分配内存不一定马上放入管理对象,因为有其它函数干扰,这不是独立语句. 条款18:让接口容易被正确使用,不易被误用. 1.好的接口很容易被正确使用,不容易被误用.你应该在你的所有接口中努力达成这些性质. 2.“促进正确使用”的办法包括接

effective c++学习笔记条款35-37

#include<iostream> using namespace std; class A { public: void asd() { pri(); } private: /*virtual*/ void pri() { cout << "基类函数" << endl; } }; class B :public A { private: void pri() /*override*/ { cout << "派生类函数&quo

effective c++学习笔记条款29-31

条款29:为异常安全而努力是值得的[回顾] 1.异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏,这样的函数分为3种可能的保证:基本型,强烈型,不抛异常型 2.“强烈保证”往往能通过copying and swap 来实现出来,但并非所有函数都可实现或者具备现实意义. 3.函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全中”的最弱者.