Effect C++ 笔记 【4 Designs and Declarations】
2010-12-30 22:49
134 查看
4 设计与声明
条款18:让接口容易被正确使用,不易被误用
//这章举了几个例子,一个是用新的 struct 限制输入参数; 一个是智能指针保证 资源释放。 需要看完 STL 再回头看。Tips:
“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。 //全面考虑,各种可能得古怪的输入
接口一致性,以及与内置类型行为兼容,有助于 “正确使用”。
任何接口如果要求客户必须记得做某些事情,就是有 “不正确倾向”
“阻止误用” 的办法 : 建立新类型,限制类型操作( const ),束缚对象值( emule ),消除客户资源管理责任( 智能指针 )
tr1::shared_ptr 支持定制型删除器。 可防范 DLL问题,可被用来自动解除互斥锁等等。
条款19:设计 class 犹如设计 type
如何设计高效的 classes,你要面对以下提问:新 type 的对象如而后被创建和销毁? —— 构造函数、析构函数、内存分配和释放(new、delete)
对象的初始化和对象的赋值该有什么样的区别? —— 构造函数、赋值操作符 (别混淆初始化和赋值,调款4)
新 type 的对象如果被pass by value,意味着什么? —— 拷贝构造函数定义一个type的pass by value 的实现
什么是新 type 的“合法值”? —— 成员函数必须进行错误检查工作
你的新 type 需要配合某个继承图系么? —— 如果你继承既有的class,就得受父类的约束,特别是“他们的函数是virtual”的影响(条款34、36)。如果其他类继承你的class,那会影响你所声明的函数-尤其是析构函数-是否为virtual(条款7)。
你的新 type 需要什么样的转换? —— 如果希望允许类型 T1 之物被隐式转换为 T2 之物,就必须在 class T1 内写一个类型转换函数(operator T2)或在 class T2 内写一个 non-explicit-one-argument(可被单一实参调用)的构造函数。如果你只允许explicit构造函数存在,就得写出专门负责执行转换的函数。
什么样的操作符和函数对此 type 而言是合理的? —— 某些该是 member 函数,某些则否 (23、24、46条款)
什么样的标准函数应该被屏蔽? —— 那些正是你必须声明为 private的
谁该取用新 type 的成员? —— 这个问题帮助你决定,那个成员为public,那个为protect或private。以及哪个class或function是friend。
什么是新 type 的“未声明接口”? —— 对效率、异常安全性(见条款29)以及资源运用提供何种保证?没看懂
你的新 type 有多一般化? —— 或许应该是一个 class template
真的需要一个新 type 吗?
条款20:最好以 pass-by-reference-to-const 替换 pass-by-value
传值调用,由拷贝构造函数完成。使用传引用 代替 传值 两个作用:
对于自定义类型,节约资源(剩了拷贝构造,和析构)
防止 切割问题(slicing problem) : 基类指针指向一个派生类对象 ,然后这个指针被函数传值调用,那么拷贝构造只复制了该对象的基类部分。
Tips:
尽量
以 传const引用
代替 传值。 高效且避免切割问题。
只对
内置类型,和 STL 的迭代器、函数对象,使用传值
。
条款21:必须返回对象时,别妄想返回其 reference
所谓 reference 只是个别名,代表某个【既有】对象。任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么?因为他一定是某物的另一个名称。
Tips:
绝不要返回 pointer 或 reference 指向一个 local stack 对象 // 局部变量销毁后,指针悬挂了
或 heap-allocated对象 // 可能无法正确的 delete 掉这个对象 ,比如 w=x*y*z,operator*返回引用的话,x*y返回的引用就无法delete
或 指向 local static 对象而又必须使用很多这样的对象 // 资源浪费
简单办法,【返回一个新对象】!
条款22:将成员变量声明为private
结论很简单:【成员变量应该是 private】Tips:
将成员变量声明为 private。 好处:
访问数据一致性: 如果public接口内每样东西都是函数,客户就不需要在打算访问class成员时犹豫是否使用小括号,因为样东西都是函数。
细微划分访问控制:如果成员变量设为public,每个人都可以读写它。但是以函数取得或者设置其值,可以实现各种控制。
为“所有可能得实现”提供弹性:例如,可使成员变量被读写时通知其他对象、验证class约束条件、函数前提和事后状态、多线程环境下执行同步控制......
protected 并不比 public 更具封装性: 一旦你将一个成员变量声明为 public或protected 而客户开始使用它,就很难再改变那个成员变量涉及的一切。
条款23:宁以 non-member && non-friend 替换 member 函数
考虑封装性:作为一种粗糙的量测,越多函数可以访问这个数据,那数据的封装性就越低。条款22,曾说,成员变量应为private。因为如果它不是,就有无限的函数可以访问他。而 能够访问private成员变量的函数 只有class的 menber函数加上 friend函数
而已。
因此,“非成员非友元”函数比“成员函数”有更大的封装性。(注意是'非成员且非友元'。 友元函数和成员函数的访问权利是相同的)
C++,比较自然地做法是: 让这种 为对象提供便利的函数 成为一个non-member函数且位于 其服务的类 的同一个namespace内。
要知道,namespace和classes 不同,前者可以跨越多个源码文件而后者不能
。
这正是C++标准库的组织方式。 标准库不是拥有单一的、整体的、庞大的 <C++StandardLibrary>头文件并在其中内含std命名空间里的每个东西,而是有数十个头文件,每个头文件声明std的某些机能。 但这种切割方式并不适用于class成员函数,因为一个class必须整体定义,不能分割为片段。
将所有便利函数放在多个头文件内,但隶属同一个命名空间,意味着客户可以轻松扩展这一组便利函数。 需要做的就是添加更多 非成员非友元函数 到此命名空间。
条款24: 弱所有参数皆需类型转换,请为此采用 non-member 函数
隐式转换 发生在 函数参数列表中。 对于member函数,隐式转换发生在 “被调用的成员函数所隶属的那个对象”--即this对象--的那个参数表。(这个主要是对operater说的,因为它们容易被重载。因此需要分清是member的还是non-member,接受的是两个参数lhs,rhs 还是 只有其中之一)【重要】: member函数的反面 是 non-member, 而不是 friend
。 与某class有关的函数如果不该成为member,不是就一定是friend。non-member函数也完全可以通过class的public接口完成一定功能
。
条款25:考虑写出一个不抛异常的 swap 函数
典型的标准程序库提供的 swap 写法就是 一个中间temp变量拷贝构造和copy assignment操作:namespace std{ template<typename T> void swap( T& a, T& b) { T temp(a); //调用拷贝构造函数 a=b; // 拷贝赋值操作符 b=temp; } }
但是,复制中,常见就是“以指针指向一个对象,内含真正数据”那种类型。 所以为“pimple手法”(pointer to implementation)。 这样写Widget class,看起来想这样:
class WidgetImpl { public: ... private: int a, b, c; // 可能有许多数据 std::vector<double> v; //意味着复制很长时间 ... }; class Widget { public : Widget ( const Widget& rhs) // 拷贝构造 Widget& operater=(const Widget& rhs) { ...... *pImpl = *( rhs.pImpl); ...... } ...... private: WidgetImpl* pImpl
调换两个 Widget 对象值,我们需要做的 就是调换两个 pImpl 指针, 但缺省的swap算法不知道这点。
我们希望告诉 std::swap : 当 Widget 被置换时, 该做的是置换其内部 pImpl 指针。
一个做法是: 将 std::swap 针对 Widget 特化 !
下面是基本构想,但目前这个形式无法通过编译:
namespace std { template <> // 表示一个全特化版本 void swap<Widget> ( Widget& a, Widget& b) // 表示这一特化版本是针对T是 Widget 设计的 { swap (a.pImpl, b.pImpl); // 只置换指针就好 } }
【"template <>" 表示它是 std::swap 的一个全特化(total template specialization)版本,函数名称之后的“<Widget>”表示这一特化版本系针对“T是Widget”而设计。】
【通常,我们不允许改变 std 命名空间内的任何东西
, 但是
可以为 标准template 制造特化版本
。】
这个函数无法编译,因为它企图访问 a和b 的private 成员 pImpl。 我们可以将这个特化声明为 friend, 但这里
我们令 Widget 声明一个名为 swap 的 public 成员函数。 然后将 std::swap特化, 令它调用该成员函数:
class Widget { public : ... void swap ( Widget& other) { using std::swap ; // 稍后解释 swap (pImpl, other.pImpl); } ... }; namespace std { template<> //修订的std::swap 特化版本 void swap<Widget> ( Widget& a, Widget& b) { a.swap(b); // 调用Widget类的 swap成员函数 } }
这种做法与 STL 容器有一致性
, 【 所有 STL 容器
也都提供
有 public swap 成员函数 和 std::swap 特化版本(用
以调用前者)。】
假设Widget 和 WidgetImpl 都是 class templates 而非 class。那么,在Widget内放个swap成员函数没问题, 但在特化 std::swap 时遇上乱流,我们想写成这样:
namespace std { template<typename T> void swap< Widget<T> >(Widget& a, Widget b) // 不合法!!! { a. swap(b) ;} }
看起来合理,但不合法。 我们企图偏特化一个 function template 。【 但C++只允许对 class template 偏特化 】
当你打算偏特化一个 function template 时,管用的做法是简单地为它添加一个重载版本。
一般而言,重载 function template 没有问题。但 std 是个特殊的命名空间
。客户可以全特化std内的template
, 但不能添加
新的template(其实是任何东西)到std里
。
那么,如何是好? 我们要让其他人调用swap时能取得我们提供的高效的template特定版本。 还是声明一个 non-member swap 让他调用 member swap, 但不再将那个non-member swap 声明为 std::swap 的特化版或重载版。
假设 Widget 所有相关机能都在 命名空间 WidgetStuff 内, 那整个结果像这样:
namespace WidgetStuff { ... template<typename T> class Widget { ... }; // 同前 ,内含 swap 成员函数 ...... template<typename T> //非成员 swap函数 void swap ( Widget& a, Widget& b) // 这里不属于std命名空间,所以 根据“实参取决之查找规则” ,Widget位于WidgetStuff命名空间内,就会找到这里的这个 Widget专用版本swap { a.swap(b); } }
这个做法对 class 和 class template都行的通。 (如果,没有额外使用命名空间,上述每件事仍适用。 但,何必在 global 空间内塞满各式各样的 class , template,function, enum以及typedef 名称呢)
但是,你还应该为 class 特化 std::swap。 因为,如果你想让你的“class专属版”swap在尽可能多的语境下(就是说,在任何时候都能找到最合适的一个版本)被调用,你需要同时在该 class 所在命名空间内写一个 non-member版本,及一个 std::swap特化版本。
从客户角度看,假设你正写一个 function template, 其内需要置换两个对象:
template< typename T> void doSomething ( T& obj1, T& obj2 ) { ... swap( obj1, obj2 ); ... }
应该调用哪个 swap ?????
我们希望的是:
template<typename T> void doSomething (T& obj1, T& obj2) { using std::swap; //令 std::swap 在此函数内可用 ... swap( obj1 , obj2 ); // 这就可以为 T 类型 调用最佳 swap 版本 ... }
这里,C++的名称查找法则 确保将找到 global 作用域 或 T 所在之命名空间内的 任何T专属swap。
如果 T 是 Widget 类型,并位于 命名空间 WidgetStuff 内, 编译器使用"实名参数取决之查找法则"( argument-dependent lookup) 找出 WidgetStuff 内的 swap。
如果 T 没有专属swap存在,编译器使用 std内的 swap 。 // 因为使用了using 声明
如果 T 存在swap专属 特化版本, 那么因为你已这对 T 特化了 std::swap, 因此特化版被调用。
【总结】:
如果 swap 缺省实现代码对你的 class 或 class template 提供可接受的效率,你不用做任何事情。
如果 swap 缺省版本效率不足 (几乎总意味着 使用了 指针指向内容 ‘pimpl’ 的手法):
提供 public swap 成员函数。 这个函数绝不该抛出异常。
在你的 class 所在命名空间内,提供 non-member swap, 并用它 调用 1 中的 swap 成员函数。
如果你是class(而不是 class template), 特化 std::swap。并用它 调用 1 中的 swap 成员函数。
调用swap, 请包涵一个 using 声明式。 以便 让std::swap在函数内可见。
相关文章推荐
- C++ Chapter 4. Designs and Declarations
- effective C++学习(Designs and Declarations)
- Conclusion for Designs and Declarations
- Effective C++ - Designs and Declarations
- Effect C++ 笔记 【2 Constructors Destructors and Assignment Operators】
- 【转】R-CNN学习笔记2:Rich feature hierarchies for accurate object detection and semantic segmentation
- 论文笔记之---Joint Detection and Identification Feature Learning for Person Search
- 论文笔记-Person Re-identification Past, Present and Future
- 【笔记】Core GameObjects, components, and concepts relating to Unity UI development include
- 论文笔记:Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks
- Perceptual Losses for Real-Time Style Transfer and Super-Resolution----论文笔记
- ICLR2017 paper: FASTER CNNS WITH DIRECT SPARSE CONVOLUTIONS AND GUIDED PRUNING 笔记
- Quartz学习笔记(三)—— TriggerListeners and JobListeners
- 【Head First Servlets and JSP】笔记22:直接从请求到JSP & 获取Person的嵌套属性
- image and video processing 听课笔记(六)
- 高性能MySQL笔记-第4章Optimizing Schema and Data Types
- WPF and Silverlight 学习笔记(二十二):使用代码实现绑定、绑定数据的验证
- The GNU C Library Reference Manual—Virtual Memory Allocation And Paging笔记(3)
- 《学习opencv》笔记——矩阵和图像处理——cvGEMM,cvGetCol,cvGetCols and cvGetDiag