1ms Faster
Home
  • Program

    • Lean in C++
  • Perfromance Engineering

    • [>>>>>]
  • Reading Note

    • [>>>>>]
  • ComputeArch

    • [_]
  • Compiler

    • [_]
  • System

    • [_]
Authoring
  • Categories
  • Tags
GitHub (opens new window)

Quincy Jet

We are.
Home
  • Program

    • Lean in C++
  • Perfromance Engineering

    • [>>>>>]
  • Reading Note

    • [>>>>>]
  • ComputeArch

    • [_]
  • Compiler

    • [_]
  • System

    • [_]
Authoring
  • Categories
  • Tags
GitHub (opens new window)
  • Lean in c++

  • Performance Engineering

  • Misc

  • Reading Notes

    • Effective C++

      • 习惯C++
      • 构造,析构, 和重载赋值运算符
      • 资源管理
      • 设计与声明
      • 实现Implementations
        • 实现, Implementations
          • Item 26:尽可能延后变量定义式的出现时间 Postpone variable definitionsas long as possible.
          • Item 27:尽量少做Casting, Minimize casting.
          • Item 28:避免返回handles指向对象内部成分 Avoid returning"handles"toobject internals.
          • Item 29:为“异常安全”而努力是值得的 Strive for exception-safe code.
          • Item 30:透彻了解inlining的里里外外 Understand the ins and outs of inlining.
          • Item 31:将文件间的编译依存关系降至最低 Minimize compilation dependencies between files.
      • 继承与OOP
      • 模板,Templates and GP
      • 定制new和delete
      • 杂项, Misc
    • More Effective C++

    • 《C++ 性能优化指南》
  • Wiki
  • Reading Notes
  • Effective C++
Quincy Jet
2022-06-27
Content

实现Implementations

# 实现, Implementations

# Item 26:尽可能延后变量定义式的出现时间 Postpone variable definitionsas long as possible.

  1. 延迟变量定义的出现时间, 尽量用到的时候再定义;

    1. 每当定义一个变量时,就会带来构造和析构的运行成本,因为代码运行到定义时会调用对象的构造函数,当离开作用域时便会调用析构函数。
    2. 把它的定义尽量往后推迟,直到我们100%确定要用到:
    //用:
    string encrypted(password);
    //替换掉:
    string encrypted;
    encrypted = password;
    
    1
    2
    3
    4
    5

    我们不仅仅需要把变量的定义推迟到100%要用到的地方,还要把它推迟到100%有构造参数可用的时候。这样做既可以避免不必要的构造和析构过程,也能节省默认构造再赋值的成本。而且这样的代码也更可读,因为变量定义在了真正需要它的环境下。

    对于一个变量只在循环里用到,把它定义在循环外面然后每次在里面赋值好,即代码A,还是直接在里面定义呢,即代码B?

    //代码A,在外面定义
    Widget w;
    for(int i=0; i<n, i++){
    w=...;
    ...
    }
    
    //代码B,在里面定义
    for(int i=0; i<n; i++){
    Widget w(...);
    ...
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    那么我们就来分析一下A和B各自的运行成本:

    A: 1个构造 + n个赋值 + 1个析构
    B: n个构造 + n个析构
    
    1
    2

    对于赋值成本低于(构造+析构)的类,A是更高效的选择,尤其是当n很大的时候。反之如果赋值成本大于(构造+析构),B则是更好的选择。但是对象在A的作用域比在B要大,有时是不利于程序的可读性和可维护性的。因此除非你知道赋值成本低于(构造+析构),而且这段代码要更注重效率,那么我们应该默认使用B。

# Item 27:尽量少做Casting, Minimize casting.

  1. 少做cast转型动作, 即使要用也尽量使用C++自己的那四个转型, 这里涉及几种C++形式的转换的作用和相关的优缺点

    1. C++的类型转换有3种方式,C风格,函数风格和C++风格:
(T)expression  //C风格
T(expression) //函数风格

//C++风格
static_cast<T>(expression)
dynamic_cast<T>(expression)     //T必须是指向多态类型的指针或引用
const_cast<T>(expression)       //T必须是指针或引用
reinterpret_cast<T>(expression) //T必须是指针或引用
1
2
3
4
5
6
7
8

如果一定要转型, 请放在函数里面, 不要让用户在使用这个函数的时候还要惦记转型的事情;

# Item 28:避免返回handles指向对象内部成分 Avoid returning"handles"toobject internals.

  1. 避免返回一个handler, 这个handler却有指向对象的内部成分,

    1. 如果一个函数返回了指向储存在对象外部的数据成员的引用,即使这个函数声明为了const,调用这个函数的人也能修改这个成员(见第3章 (opens new window)bitwise constness的局限性)

    2. 避免返回的是一个空悬的指针;

    3. 避免可以使用调用的操作来对函数内部的元素进行修改, 可以指定返回值是一个const, 这样用户就不能修改了;

      class Rectangle{
        public:   
          //现在返回的是const Point&   
          const Point& upperLeft() const{return pData->ulhc;}
          const Point& lowerRight() const{return pData->lrhc;}
        ... 
      };
      
      1
      2
      3
      4
      5
      6
      7
    4. 避免返回指向内部成员的"句柄",包括指针,引用,迭代器。不返回"句柄"能增强封装性,让const函数真正const,也能减少"野句柄"。

# Item 29:为“异常安全”而努力是值得的 Strive for exception-safe code.

  1. 注意异常安全, (exceptional C++里面有很多异常安全的相关介绍)

    • 异常安全

      异常安全***的意思就是,当程序在异常发生的时候,程序可以回退的很干净。什么是回退的很干净呢?其实就是函数在发生异常的时候***不会泄露资源***或者***不会发生任何数据结构的破坏。

      不泄露任何资源

      不允许破坏任何数据

  • 异常安全的函数即使在抛出异常时也不会泄露资源,损坏数据结构。这种安全性有三种级别,基本保证,强保证和不抛出保证

    • 提供基本保证(basic guarantee)的函数可以保证即使抛出了异常,函数也能在有效的状态下运行,没有对象或数据损坏,所有对象也保持内部一致,依然满足类不变量(class invariant),但是程序本身则可能处于不确定状态。例如用户使用我们changeBackground方法时抛出了异常,PrettyMenu对象可能依然持有原来的背景,或者持有默认的背景,但具体哪个则是不确定的。
    • 提供强保证(strong guarantee)的函数可以保证如果函数抛出了异常,程序的状态不会改变。这就意味着对强保证函数的调用是原子性的(atomic),如果成功了就成功了,如果失败了就像什么都没有发生一样。强保证函数比基本保证函数更容易使用,因为强保证函数只能导致两种状态,成功或者不变,而基本保证的函数可能引向任何状态。
    • 提供不抛出保证(nothrow guarantee)的函数保证永远不会抛出异常。例如所有对于基本类型(int,指针等等)的操作都提供不抛出保证。它是异常安全代码的基础。
  • copy and swap是实现强保证的有效方法,但给所有的函数加上强保证显然也不是实际的选择

  • 函数的异常安全性遵循木桶原理,函数的最强安全性取决于它所调用操作的最弱安全性

# Item 30:透彻了解inlining的里里外外 Understand the ins and outs of inlining.

  1. 对inline的里里外外需要透彻了解;
  • 尽量inline, 也许compiler就能够执行context相关的inline优化;
  • 定义在类里面的函数, 自动inline
  • inline会让代码文件变大, 会导致更多的换页行为(paging),降低 i cache的命中率, 如果函数比较小, inline对 icache miss的影响会稍微比较小, 但是inline的函数比较大, 就有可能会得不偿失;
  • 虚函数的inline, 大多都不会生效
  • inline不仅会导致代码变大, 而且一旦inline的函数发生改变, 所有用到inline function的地方都要重新编译
  • inline函数一般放在头文件里面, template一般也都是发那个在头文件里面;
  • template要避免无脑成为一个inline函数, 它的代价是很大的;因为可能程序中很多地方都用到了这个模板

# Item 31:将文件间的编译依存关系降至最低 Minimize compilation dependencies between files.

  1. 尽量减少文件之间的编译依赖关系;不然修改之后编译, 会导致很多部件都一起编译了【缺demo】
  • 头文件应该仅有声明不要有定义;
  • 可以只有声明, 而没有定义, 这会导致一个方法: 前向声明, 前向声明只是把#include的内容, 放在代码的前面;
  • 一般使用pointer to implementation 和纯虚函数的工厂方法来解决, 前者叫handle class, 后者叫interface class;
设计与声明
继承与OOP

← 设计与声明 继承与OOP→

Copyright © 2017-2023 Quincy Jet | MIT License
  • Auto
  • Light
  • Dark
  • Read