Effective C++ 笔记(4)

发布于 2023-09-19 07:36:55 字数 15247 浏览 33 评论 0

21. 必须返回对象时,别妄想返回其 reference

Don’t try to return a reference when you must return an object

class Rational {
 public:
  Rational(int numerator = 0, int denominator = 1)
      : n(numerator), d(denominator) {}

 private:
  int n, d;  // 分子(numerator)和分母(denominator)
  // 返回 const Rational 可以预防"没意思的赋值动作": Rational a, b, c; (a * b) =
  // c;
  friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
    return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
  }
};

所谓 reference 只是个名称,代表某个既有对象。任何时候看到一个 reference 声明式,你都应该立刻问自己,它的另一个名称是什么?因为它一定是某物的另一个名称。任何函数如果返回一个 reference 指向某个 local 对象,都将一败涂地。(如果函数返回指针指向一个 local 对象,也是一样。)

请记住:

绝不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。

22. 将成员变量声明为 private

Declare data members private

请记住:

  • 切记将成员变量声明为 private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供 class 作者以充分的实现弹性。
  • protected 并不比 public 更具封装性。

23. 宁以 non-member、non-friend 替换 member 函数

Prefer non-member non-friend functions to member functions

请记住:宁可拿 non-member non-friend 函数替换 member 函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。

24. 若所有参数皆需类型转换,请为此采用 non-member 函数

Declare non-member functions when type conversions should apply to all parameters

class Rational24 {
 public:
  Rational24(int numerator = 0, int denominator = 1) {
  }  // 构造函数刻意不为 explicit,允许 int-to-Rational24 隐式转换
  int numerator() const { return 1; }    // 分子(numerator)的访问函数
  int denominator() const { return 2; }  // 分母(denominator)的访问函数

  /*const Rational24 operator* (const Rational24& rhs) const
  {
          return Rational24(this->n * rhs.numerator(), this->d *
  rhs.denominator());
  }*/

 private:
  int n, d;
};

const Rational24 operator*(const Rational24& lhs,
                           const Rational24& rhs)  // non-member 函数
{
  return Rational24(lhs.numerator() * rhs.numerator(),
                    lhs.denominator() * rhs.denominator());
}

int test_item_24() {
  Rational24 oneEighth(1, 8);
  Rational24 oneHalf(1, 2);
  Rational24 result = oneHalf * oneEighth;  // 很好
  result = result * oneEighth;              // 很好

  result = oneHalf * 2;  // 很好,隐式类型转换(implicit type conversion)
  result = 2 * oneHalf;  // 错误, only non-member function success

  // 以对应的函数形式重写上述两个式子
  // result = oneHalf.operator*(2); // 很好, only member function success
  // result = 2.operator*(oneHalf); // 错误, only non-member function success

  result = operator*(2, oneHalf);  // 错误, only non-member function success

  return 0;
}

无论何时如果你可以避免 friend 函数就该避免。

请记住:如果你需要为某个函数的所有参数(包括被 this 指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个 non-member。

25. 考虑写出一个不抛异常的 swap 函数

Consider support for a non-throwing swap

class WidgetImpl {  // 针对 Widget25 数据而设计的 class
 public:
 private:
  int a, b, c;  // 可能有许多数据,意味复制时间很长
  std::vector<double> v;
};

class Widget25 {  // 这个 class 使用 pimpl(pointer to implementation)手法
 public:
  Widget25(const Widget25& rhs) {}
  Widget25& operator=(
      const Widget25& rhs)  // 复制 Widget25 时,令它复制其 WidgetImpl 对象
  {
    *pImpl = *(rhs.pImpl);

    return *this;
  }

  void swap(Widget25& other) {
    using std::swap;
    swap(pImpl, other.pImpl);  // 若要置换 Widget25 就置换其 pImpl 指针
  }

 private:
  WidgetImpl* pImpl;  // 指针,所指对象内含 Widget25 数据
};

// std::swap 针对 Widget25 特化版本
namespace std {
template <>
void swap<effective_cplusplus_::Widget25>(effective_cplusplus_::Widget25& a,
                                          effective_cplusplus_::Widget25& b) {
  a.swap(b);  // 若要置换 Widget25,调用其 swap 成员函数
}
}  // namespace std

所谓 swap(置换)两对象值,意思是将两对象的值彼此赋予对方。缺省情况下 swap 动作可由标准程序库提供的 swap 算法完成。

一般而言,重载 function template 没有问题,但 std 是个特殊的命名空间,其管理规则也比较特殊。客户可以全特化 std 内的 template,但不可以添加新的 template(或 class 或 function 或其它任何东西)到 std 里头。

首先,如果 swap 的缺省实现码对你的 class 或 class template 提供可接受的效率,你不需要额外做任何事。其次,如果 swap 缺省实现版的效率不足,试着做以下事情:(1). 提供一个 public swap 成员函数,让它高效地置换你的类型的两个对象值。这个函数不该抛出异常。(2). 在你的 class 或 template 所在的命名空间内提供一个 non-member swap,并令它调用上述 swap 成员函数。(3). 如果你正在编写一个 class(而非 class template),为你的 class 特化 std::swap。并令它调用你的 swap 成员函数。 最后,如果你调用 swap,请确定包含一个 using 声明式,以便让 std::swap 在你的函数内曝光可见,然后不加任何 namespace 修饰符,赤裸裸地调用 swap。

请记住:

  • 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个 member swap,也该提供一个 non-member swap 用来调用前者。对于 classes(而非 template),也请特化 std::swap。
  • 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何”命名空间资格修饰”。
  • 为用户定义类型进行 std template 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西。

26. 尽可能延后变量定义式的出现时间

Postpone variable definitions as long as possible

你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造(和析构)非必要对象,还可以避免无意义的 default 构造行为。

请记住:尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。

27. 尽量少做转型动作

Minimize casting

const_cast 通常被用来将对象的常量性移除(cast away the constness)。它也是唯一有此能力的 C++-style 转型操作符。

dynamic_cast 主要用来执行”安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。

reinterpret_cast 意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个 pointer to int 转型为一个 int。

static_cast 用来强迫隐式转换(implicit conversions),例如将 non-const 对象转为 const 对象,或将 int 转为 double 等等。它也可以用来执行上述多种转换的反向转换,例如将 void*指针转为 typed 指针,将 pointer-to-base 转为 pointer-to-derived。但它无法将 const 转为 non-const,这个只有 const_cast 才办得到。

请记住:

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_cast。如果有个设计需要转型动作,试着发展无须转型的替代设计。
  • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
  • 宁可使用 C++-style(新式)转型,不要使用旧式转型(C 风格转型)。前者很容易辨识出来,而且也比较有着分门别类的职掌。

28.避免返回 handles 指向对象内部成分

Avoid returning handles to object internals

class Point {  // 这个 class 用来表述"点"
 public:
  Point(int x, int y) {}
  void setX(int newVal) {}
  void setY(int newVal) {}
};

struct RectData {  // 这些"点"数据用来表现一个矩形
  Point ulhc;      // ulhc = "upper left-hand corner"(左上角)
  Point lrhc;      // lrhc = "lower right-hand corner"(右上角)
};

class Rectangle {
 public:
  Rectangle(const Point&, const Point&) {}
  Point& upperLeft() const { return pData->ulhc; }
  Point& lowerRight() const { return pData->lrhc; }

  // 有了这样的改变,客户可以读取矩形的 Point,但不能涂写它们
  // 但即使如此,也可能导致 dangling
  // handles(空悬的号码牌):这种 handles 所指东西(的所属对象)不复存在
  // const Point& upperLeft() const { return pData->ulhc; }
  // const Point& lowerRight() const { return pData->lrhc; }

 private:
  std::shared_ptr<RectData> pData;
};

int test_item_28() {
  Point coord1(0, 0);
  Point coord2(100, 100);
  const Rectangle rec(coord1, coord2);  // rec 是个 const 矩形,从(0,0)到(100,100)

  // upperLeft 的调用者能够使用被返回的 reference(指向 rec 内部的 Point 成员变量)
  // 来更改成员,但 rec 其实应该是不可变的(const)
  rec.upperLeft().setX(50);  // 现在 rec 却变成从(50,0)到(100,100)

  return 0;
}

reference、指针和迭代器统统都是所谓的 handles(号码牌,用来取得某个对象),而返回一个”代表对象内部数据”的 handle,随之而来的便是”降低对象封装性”的风险。

通常我们认为,对象的”内部”就是指它的成员变量,但其实不被公开使用的成员函数(也就是被声明为 protected 或 private 者)也是对象”内部”的一部分。因此也应该留心不要返回它们的 handles。这意味你绝对不该令成员函数返回一个指针指向”访问级别较低”的成员函数。

请记住:避免返回 handles(包括 reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const,并将发生”虚吊号码牌”(dangling handles)的可能性降至最低。

29. 为 异常安全 而努力是值得的

Strive for exception-safe code

“异常安全”有两个条件:(1).不泄漏任何资源。(2).不允许数据败坏。

异常安全函数(Exception-safe functions)提供以下三个保证之一:

  1. 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的 class 约束条件都继续获得满足)。然而程序的现实状态(exact state)恐怕不可预料。
  2. 强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功;如果函数失败,程序会回复到”调用函数之前”的状态。
  3. 不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如 int,指针等等)身上的所有操作都提供 nothrow 保证。这是异常安全码中一个必不可少的关键基础材料。

异常安全码(Exception-safe code)必须提供上述三种保证之一。如果它不这样做,它就不具备异常安全性。

有个一般化的设计策略很典型地会导致强烈保证,这个策略被称为 copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。但一般而言它并不保证整个函数有强烈的异常安全性。

请记住:

  • 异常安全函数(Exception-safe function)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
  • 强烈保证 往往能够以 copy-and-swap 实现出来,但 强烈保证 并非对所有函数都可实现或具备现实意义。
  • 函数提供的”异常安全保证”通常最高只等于其所调用之各个函数的”异常安全保证”中的最弱者。

30. 透彻了解 inlining 的里里外外

Understand the ins and outs of inlining

inline void f() {}  // 假设编译器有意愿 inline“对 f 的调用”

int test_item_30() {
  void (*pf)() = f;  // pf 指向 f

  f();   // 这个调用将被 inlined,因为它是一个正常调用
  pf();  // 这个调用或许不被 inlined,因为它通过函数指针达成

  return 0;
}

inline 函数背后的整体观念是,将”对此函数的每一个调用”都以函数本体替换之。

过度热衷 inlining 会造成程序体积太大(对可用空间而言)。即使拥有虚内存,inline 造成的代码膨胀亦会导致额外的换页行为(paging),降低指令高速缓存装置的击中率(instruction cache hit rate),以及伴随这些而来的效率损失。

inline 只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于 class 定义式内。这样的函数通常是成员函数。friend 函数如果被定义于 class 内,它们也是被隐喻声明为 inline。明确声明 inline 函数的做法则是在其定义式前加上关键字 inline。

inline 函数通常一定被置于头文件内,因为大多数建置环境(build environments)在编译过程中进行 inlining,而为了将一个”函数调用”替换为”被调用函数的本体”,编译器必须知道那个函数长什么样子。

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

templates 通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。(这其实也不是世界一统的准则。某些建置环境可以在链接期才执行 template 具现化。只不过编译期完成具现化动作比较常见。)template 的具现化与 inlining 无关。

大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数 inlining,而所有对 virtual 函数的调用(除非是最平淡无奇的)也都会使 inlining 落空。

一个表面上看似 inline 的函数是否真是 inline,取决于你的建置环境,主要取决于编译器。大多数编译器提供了一个诊断级别:如果它们无法将你要求的函数 inline 化,会给你一个警告信息。为数 ining 个函数长声明 entshit rate

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

实际上构造函数和析构函数往往是 inlining 的糟糕候选人。

inline 函数无法随着程序库的升级而升级。换句话说如果 f 是程序库内的一个 inline 函数,客户将”f 函数本体”编进其程序中,一旦程序库设计者决定改变 f,所有用到 f 的客户端程序都必须重新编译。

一开始先不要将任何函数声明为 inline,或至少将 inlining 施行范围局限在那些”一定成为 inline”或”十分平淡无奇”的函数身上。慎重使用 inline 便是对日后使用调试器带来帮助。

请记住:

  • 将大多数 inlining 限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
  • 不要只因为 function templates 出现在头文件,就将它们声明为 inline。

31. 将文件间的编译依存关系降至最低

Minimize compilation dependencies between files

标准程序库组件不该被前置声明。

设计策略:

  1. 如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects。你可以只靠一个类型声明式就定义出指向该类型的 references 和 pointers;但如果定义某类型的 objects,就需要用到该类型的定义式。
  2. 如果能够,尽量以 class 声明式替换 class 定义式。注意,当你声明一个函数而它用到某个 class 时,你并不需要该 class 的定义;纵使函数以 by value 方式传递该类型的参数(或返回值)亦然。
  3. 为声明式和定义式提供不同的头文件。为了促进严守上述准则,需要两个头文件,一个用于声明式,一个用于定义式。当然,这些文件必须保持一致性,如果有个声明式被改变了,两个文件都得改变。

Handle classes(使用 pimpl idiom(pimpl 是”pointer to implementation”的缩写))和 Interface classes(特殊的 abstract base class(抽象基类))解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性(compilation dependencies)。

在 Handle classes 身上,成员函数必须通过 implementation pointer 取得对象数据。那会为每一次访问增加一层间接性。而每一个对象消耗的内存数量必须增加 implementation pointer 的大小。最后,implementation pointer 必须初始化(在 Handle class 构造函数内),指向一个动态分配得来的 implementation object,所以你将蒙受因动态内存分配(及其后的释放动作)而来的额外开销,以及遭遇 bad_alloc 异常(内存不足)的可能性。

至于 interface classes,由于每个函数都是 virtual,所以你必须为每次函数调用付出一个间接跳跃(indirect jump)成本。此外 interface class 派生的对象必须内含一个 vptr(virtual table pointer),这个指针可能会增加存放对象所需的内存数量—-实际取决于这个对象除了 interface class 之外是否还有其它 virtual 函数来源。

最后,不论 handle classes 或 interface classes,一旦脱离 inline 函数都无法有太大作为。函数本体为了被 inlined 必须(很典型地)置于头文件内,但 handle classes 和 interface classes 正是被设计用来隐藏实现细节如函数本体。

然而,如果只因为若干额外成本便不考虑 handle classes 和 interface classes,将是严重的错误。

请记住:

  1. 支持 编译依存性最小化 的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是 handle classes 和 interface classes。
  2. 程序库头文件应该以”完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及 templates 都适用。

32. 确定你的 public 继承塑模出 is-a 关系

Make sure public inheritance models is-a

public inheritance(公开继承)意味”is-a”(是一种)的关系。

如果你令 class D(“Derived”)以 public 形式继承 class B(“Base”),你便是告诉 C++编译器(以及你的代码读者)说,每一个类型为 D 的对象同时也是一个类型为 B 的对象,反之不成立。

请记住:public 继承 意味 is-a。适用于 base classes 身上的每一件事情一定也适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象。

33. 避免遮挡继承而来的名称

Avoid hiding inherited names

class Base {
 private:
  int x;

 public:
  virtual void mf1() = 0;
  virtual void mf1(int) {}
  virtual void mf2() {}
  void mf3() {}
  void mf3(double) {}
};

class Derived : public Base {
 public:
  virtual void mf1() {}
  void mf3() {}
  void mf4() {}
};

class Derived33 : public Base {
 public:
  // 必须为那些原本会被遮掩的每个名称引入一个 using 声明式,否则某些你希望继承的名称会被遮掩
  using Base::
      mf1;  // 让 Base
            // class 内名为 mf1 和 mf3 的所有东西在 Derived 作用域内都可见(并且 public)
  using Base::mf3;
  virtual void mf1() {}
  void mf3() {}
  void mf4() {}
};

int test_item_33() {
  Derived d;
  int x = 0;

  d.mf1();  // 没问题,调用 Derived::mf1
  // d.mf1(x); // 错误,因为 Derived::mf1 遮掩了 Base::mf1
  d.mf2();  // 没问题,调用 Base::mf2
  d.mf3();  // 没问题,调用 Derived::mf3
  // Derived 内的函数 mf3 遮掩了一个名为 mf3 但类型不同的 Base 函数
  // d.mf3(x); // 错误,因为 Derived::mf3 遮掩了 Base::mf3

  Derived33 d2;

  d2.mf1();   // 仍然没问题,仍然调用 Derived::mf1
  d2.mf1(x);  // 现在没问题了,调用 Base::mf1
  d2.mf2();   // 仍然没问题,调用 Base::mf2
  d2.mf3();   // 没问题,调用 Derived::mf3
  d2.mf3(x);  // 现在没问题了,调用 Base::mf3

  return 0;
}

请记住:

  1. derived classes 内的名称会遮掩 base classes 内的名称。在 public 继承下从来没有人希望如此。
  2. 为了让被遮掩的名称再见天日,可使用 using 声明式或转交函数(forwarding functions)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

岁月无声

暂无简介

文章
评论
28 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文