第四章 设计与声明
18、让接口容易被正确使用,不易被误用
“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。一致性的例子:STL 容器都有 size 成员函数。不一致性对开发人员造成的心理负担,没有任何一个 IDE 可以完全抹除。
“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
std::shared_ptr 使用每个指针专属的删除器,消除“cross-DLL problem”;它还支持定制删除器,可被用来自动解除互斥锁(mutexes,见条款 14,https://my.oschina.net/umu618/blog/839649)。
19、设计 class 犹如设计 type
Class 设计就是 type 的设计,在定义一个新 type 之前,要考虑以下主题:
(1)新 type 的对象应该如何被创建和销毁?
(2)对象的初始化和赋值该有什么差别?
(3)新 type 的对象如果被以值传递(pass by value),意味着什么?
(4)什么的新 type 的合法值?setter 函数要检查错误。
(5)新 type 需要配合某个继承图系(inheritance graph)吗?这影响函数——尤其是析构函数,是否为 virtual(见条款 7,https://my.oschina.net/umu618/blog/831411)。
(6)新 type 需要什么样的转换?如果希望 T1 被隐式转换为 T2,必须在 class T1 内写一个类型转换函数(operator T2)或在 class T2 内写一个可被单一实参调用(non-explicit-one-argument)的构造函数。如果只允许 explicit 构造函数存在,就得写出专门负责转换的函数,且不得为类型转换操作符(type conversion perators)或 non-explicit-one-argument 构造函数。(条款 15 有隐式和显式转换函数的范例,https://my.oschina.net/umu618/blog/839649)
(7)什么样的操作符和函数对此新 type 而言是合理的?这决定你的 class 有哪些函数,其中哪些是 member 函数,哪些则否。(参考条款 23, 24, 26)
(8)什么样的标准函数应该驳回?声明为 private。(见条款 6,https://my.oschina.net/umu618/blog/831411)
(9)谁该取用新 type 的成员?这个问题帮你决定成员的可见性(public、protected、private)。也帮你决定哪个 classes 和/或 functions 应该是 friends,以及将它们嵌套于另一个之内是否合理。
(10)什么是新 type 的未声明接口(undeclared interface)?它对效率、异常安全性(见条款 29)以及资源运用(例如多任务锁定和动态内存)提供何种保证?你在这些方面提供的保证,将为你的 class 实现代码加上相应的约束条件。
(11)新 type 有多么一般化?new class or new class template?
(12)真的需要一个新 type 吗?如果只是定义新的子类(derived class)以便为既有 class 添加机能,那么也许单纯定义一或多个 non-member 函数或
20、宁以 pass-by-reference-to-const 替换 pass-by-value
尽量以 pass-by-reference-to-const 替换 pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem,即派生类被转化成基类时丢失派生类特有的成分)。
以上规则并不适用于内置类型,以及 STL 的迭代器和函数对象。对它们而言,pass-by-value 往往比较适当。
21、必须返回对象时,别妄想返回其 reference
不要返回 pointer 或 reference 指向一个 local stack 对象,因为离开作用域即被销毁。
不要返回 reference 指向一个 heap-allocated 对象,因为无法保证配套 delete。
不要返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。
#include <stdio.h>
#define _WINSOCK_DEPRECATED_NO_WARNINGS // to use inet_ntoa
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
in_addr a1 = {1, 2, 3, 4};
in_addr a2 = {5, 6, 7, 8};
printf_s("You think it's: %s, ", inet_ntoa(a1));
printf_s("%s\n", inet_ntoa(a2));
printf_s("But in fact it's: %s, %s\n", inet_ntoa(a1), inet_ntoa(a2));
return 0;
}
以上代码输出为:
You think it's: 1.2.3.4, 5.6.7.8
But in fact it's: 1.2.3.4, 1.2.3.4
22、将成员变量声明为 private
切记将成员变量声明为 private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供 class 作者以充分的实现弹性。
protected 并不比 public 更具封装型。
23、宁以 non-member、non-friend 替换 member 函数
宁可拿 non-member non-friend 函数替换 member 函数。这样做可以增加封装型、包裹弹性(packaging flexibility)和技能扩充性。
24、若所有参数皆需类型转换,请为此采用 non-member 函数
member 函数的反面是 non-member 函数,而不是 friend 函数。
设计 operator * 时,要能支持乘法交换律。
如果你需要为某个函数的所有参数(包括 this 指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个 non-member。从 Object-Oriented C++ 跨进 Template C++ 时,会有新争议和解法,参考条款 46。
25、考虑写出一个不抛异常的 swap 函数
通常我们不能改变 std 命名空间内的任何东西,但可以为 temlates 制造特化版本。
C++ 只允许对 class templates 偏特化(partially specialize),而对 function templates 则不许。
当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。因为成员 swap 的一个最好应用是帮助 classes 和 class templates 提供强烈的异常安全性(exception-safety)保障。条款 29 细说。
如果你提供了一个 member swap,也该提供一个 non-member swap 用来条用前者。对于 classes(而非 templates),也请特化 std::swap。
调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并不带任何“命名空间资格修饰”。
为“用户定义类型”进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std er