您的位置:首页 > 编程语言 > C语言/C++

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在函数内可见。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: