你对写一个类的细节有多在行?这条款不仅注重公然的错误,更多的是一种专业的风格。了解这些原则将会帮助你设计易于使用和易于管理的类。

JG Question

1. 什么使得接口“容易正确使用,错误使用却很难”?解释一下。

Guru Question

2. 你正在代码审查,一个程序员写了下面这个类,里面有一些不良的风格和一写具体的错误。你能发现多少?如何修正?

class complex {
public:
complex( double r, double i = )
: real(r), imag(i)
{ } void operator+ ( complex other ) {
real = real + other.real;
imag = imag + other.imag;
} void operator<<( ostream os ) {
os << "(" << real << "," << imag << ")";
} complex operator++() {
++real;
return *this;
} complex operator++( int ) {
auto temp = *this;
++real;
return temp;
} // ... more functions that complement the above ... private:
double real, imag;
};

Stop and thingking….

Solution

1. 什么使得接口“容易正确使用,错误使用却很难”?解释一下。

我们想要可能的“pit of success”,当用户以一种很自然的正确方式使用。他们很自然地写出有用,正确和高效的代码。

另一方面,我们想对于我们的用户来说很难在使用方面引起麻烦,我们想使代码的不正确使用和低效是无效的(可能的话出现编译错误)或至少是不方便和困难的。这样我们可能保护我们的用户远离意外的结果。

Scott Meyer 这方面有篇很受欢迎的文章

2. 你正在代码审查,一个程序员写了下面这个类,里面有一些不良的风格和一写具体的错误。你能发现多少?如何修正?

这个类有很多问题,甚至比我在这说出来的还多。这个条款的重点是主要强调类的结构,(类似“operator<<的规范形式什么什么样的”和“operator+应该是一个成员吗?”这样的问题)而不是指出此类在设计的缺陷。不管怎样,我会从两个可能是最重要的观察开始:
     首先,这是代码审查,但是这个开发者看起来甚至对没有对代码进行单元测试,否则的话他会发现一些很显眼的问题。
      其次,为什么他要写一个在标准库已经存在的complex类?而且,再说,标准库里类没有下面的那么多困扰。自谦一些,重用它。

Guideline:重用代码,特别是标准库中的代码,而不是自己纯手工做一个。因为它更快速、简单和安全。

可能修正complex类代码中的问题的最好方式是完全避免使用它,使用std::complex。话虽如此,这是一个很有启发性的例子,让我们看看在个例子,然后修正其中的问题。首先是构造函数:

1. 缺失默认的构造函数

complex( double r, double i =  )
: real(r), imag(i)
{ }

一旦我们提供了用户编写的构造函数,那么隐式产生的默认构造函数就会被抑制。为了“正确地易于使用”,没有一个默认的构造函数就很烦人的。在这个例子中,我们要不默认所有的参数或者提供一个complex()=default,并且使用类似double real = 0,imag = 0的初始化来声明数据成员或者只是使用构造委托complex() : complex(0){}.在这我们只是简单设置默认参数。同样,像在GotW #1中说的那样,坚持倾向于使用{}来初始化来作为一个好的习惯而不是()。在这例子中它们的意思完全相同,但是{}使得我们更一致,且在维护过程中国可能会捕捉到一些细微错误,例如打错字的情况下可能使得double 到float的变窄转换。

2.operator+使用值语义

void operator+ ( complex other ) {
real = real + other.real;
imag = imag + other.imag;
}

尽管我们对这个函数进行其他修改,我们应该传入const&给参数,因为我们只需要读取数据。

Guideline:倾向于通过使用const&来设置只读参数,如果只准备从参数做读取动作(不是从它拷贝)

3.operator+修改了this对象的值

operator+应该返回一个包含了和的complex对象而不是void和修改this对象的值。用户写类似val1 + val2不太可能通过那么怪异的语法观察val1改变了内容。Scott Meyer习惯说,在写一个值类型时,要想内建类型一个方便,比如和int一样。

4.operator+不是依据operator+=(缺失)编写

在这,operator+试着成为operator+=,它应该被拆分成operator+和operator+=,前者调用后者。

Guideline:如果你提供一个独立的operator版本(比如:operator+),总是提供一个相同的赋值版本(比如:operator+=)且依据后者来实现前者。同样,应该总是在op和op=之间保持自然的关系(op代表任意操作符)

有+=是好的,因为用户应该更喜欢使用它。即使在上面的代码中,real = real + other.real; 应该是 real += other.real;同样对于第二行。

Guideline:倾向于编写a op= b替代 a = a op b这样的语句,因为它更清晰且通常更高效。

operator+=更高效的理由是直接操作在左手端的对象,且只返回对象的引用,不是临时对象。另一方面,operator+必须返回一个临时对象。为了明白为什么,考虑下面operator+=和operator+的规范格式。

T& T::operator+=( const T& other ) {
//...
return *this;
} T operator+( T a, const T& b ) {
a += b;
return a;
}

注意到其中一个参数是传值,另一个是传引用了吗?那是因为如果你准备对一个参数进行拷贝,通常传值是好的,如果调用方传入一个临时变量时使得移动操作有效,比如:(val1 * val2) + val3。这是一个可以遵循的好习惯,即使在类型complex这样的例子中,移动和拷贝的代价是一样的,因为它不花任何效率当移动和复制都是相同的。且比传入引用然后追加一个额外的命名局部变量来说,代码更清晰。在未来的GotW中我们会看多更多关于参数传递的问题

Guideline:如果你无论如何都要从参数进行拷贝,倾向于传入一个传值的只读参数,因为这样可以对右值参数进行移动操作

使用+=来实现+让代码更简单且保证了一致的语义,在维护过程中也很少会出现分歧。

5.operator+不应该是一个成员函数

如果让operator+称为成员函数,就像这做的一样。当你决定允许从其他类型进行隐式转换,那么它在使用时就不会和我们期待的那样自然。在这,从一个double隐式转换到complex是有意义的,但是这里存在着不对称:特别是,当把complex对象加到一个数值对象时,你可以写a=b+1.0,但是a=1.0+b就是错的。因为成员函数operator+要求一个complex对象(不是double)作为它的左手参数。

最后,operator+不作为成员函数的其他理由是提供更好的封装,像Scott Meyer说的那样

Guideline:在对一个操作应该是成员和非成员之间倾向于使用这个指南:一元操作时成员;=()[]和 ->必须是成员;赋值操作(+= –= /= *=等)必须是成员;所有其他的二元操作作为非成员

6.operator<<不应该是一个成员函数

这个代码的作者应该不会想要类似my_complex << cout这样的语句吧?

void operator<<( ostream os ) {
os << "(" << real << "," << imag << ")";
}

相同的理由已经在operator+不应该是一个成员函数中阐述过了,这里对于operator<<也类似。还有一个就是第一个参数必须是ostream,不是complex。而且,这些参数应该是引用:(ostream&,const complex&)

这里有点需要注意的是,非成员operator<<应该自然地依据一个(经常是virtual)const成员来完成具体工作,通常命名为类似print的这么一个函数

7.operator<<应该返回ostream&

进一步,operator<<应该有一个类型为ostream&的返回值,返回一个流的引用是为了可以链式输出。这样的话,用户使用你的operator<<很自然地就可以写这样的代码:cout << a << b;

Guideline:从operator<<和operator>>中总是返回流的引用

8.前缀递增操作符的返回值不正确

complex operator++() {
++real;
return *this;
}

先忽略对于一个complex进行前缀递增是否有意义。如果存在这样的一个函数,那么它应该返回一个引用。这让用户代码操作更直观,且避免了不必要的低效。

Guideline:当return *this时,返回类型通常应该是引用

9.后缀递增应该依据前缀递增实现

complex operator++( int ) {
auto temp = *this;
++real;
return temp;
}

优先调用++*this,而不是重复。参考GotW #2

Guideline:为了一致性,后缀递增总是根据前缀递增实现,否则你的用户将会得到惊喜的结果。

Summary

就是这样。还有一些其他的现代C++特性可以利用在这,但对于一个一般性建议显然有点不合适。比如,这是一个值类型,不是为层级而设计的,因此我们可以通过final来防止继承。但是对于一个一般性建议没有必要告诉每个人在值类型的类应该写成final。那只会很乏味。

这有一个修正后的版本,如上面所说忽略了设计和一些风格问题:

class complex {
public:
complex( double r = , double i = )
: real{r}, imag{i}
{ } complex& operator+=( const complex& other ) {
real += other.real;
imag += other.imag;
return *this;
} complex& operator++() {
++real;
return *this;
} complex operator++( int ) {
auto temp = *this;
++*this;
return temp;
} ostream& print( ostream& os ) const {
return os << "(" << real << "," << imag << ")";
} private:
double real, imag;
}; complex operator+( complex lhs, const complex& rhs ) {
lhs += rhs;
return lhs;
} ostream& operator<<( ostream& os, const complex& c ) {
return c.print(os);
}

[译]GotW #4 Class Mechanics的更多相关文章

  1. [译]GotW #89 Smart Pointers

    There's a lot to love about standard smart pointers in general, and unique_ptr in particular. Proble ...

  2. [译]GotW #6b Const-Correctness, Part 2

         const和mutable对于书写安全代码来说是个很有利的工具,坚持使用它们. Problem Guru Question 在下面代码中,在只要合适的情况下,对const进行增加和删除(包括 ...

  3. [译]GotW #6a: Const-Correctness, Part 1

    const 和 mutable在C++存在已经很多年了,对于如今的这两个关键字你了解多少? Problem JG Question 1. 什么是“共享变量”? Guru Question 2. con ...

  4. [译]GotW #3: Using the Standard Library (or, Temporaries Revisited)

    高效的代码重用是良好的软件工程中重要的一部分.为了演示如何更好地通过使用标准库算法而不是手工编写,我们再次考虑先前的问题.演示通过简单利用标准库中已有的算法来避免的一些问题. Problem JG Q ...

  5. [译]GotW #2: Temporary Objects

        不必要的和(或)临时的变量经常是罪魁祸首,它让你在程序性能方面的努力功亏一篑.如何才能识别出它们然后避免它们呢? Problem JG Question: 1. 什么是临时变量? Guru Q ...

  6. [译]GotW #1: Variable Initialization

    原文地址:http://herbsutter.com/2013/05/09/gotw-1-solution/ 第一个问题强调的是要明白自己在写什么的重要性.下面有几行简单的代码--它们大多数之间都有区 ...

  7. [译]GotW #5:Overriding Virtual Functions

       虚函数是一个很基本的特性,但是它们偶尔会隐藏在很微妙的地方,然后等着你.如果你能回答下面的问题,那么你已经完全了解了它,你不太能浪费太多时间去调试类似下面的问题. Problem JG Ques ...

  8. [译]GotW #1: Variable Initialization 续

    Answer 2. 下面每行代码都做了什么? 在Q2中,我们创建了一个vector<int>且传了参数10和20到构造函数中,第一种情况下(10,20),第二种情况是{10, 20}. 它 ...

  9. Go开发中的十大常见陷阱[译]

    原文: The Top 10 Most Common Mistakes I've Seen in Go Projects 作者: Teiva Harsanyi 译者: Simon Ma 我在Go开发中 ...

随机推荐

  1. 关于修改tabbar的颜色的问题

    首先,项目是在故事板中搭建的,所以遇到这个问题的时候,首先是想到在故事板中找到相关的属性,确实是有一个Selected Image,但是设置了这个图片以后,运行的效果是,点击选择后,本身的image就 ...

  2. NSDate与 NSString 、long long类型的相互转化

    我的技术博客经常被流氓网站恶意爬取转载.请移步原文:http://www.cnblogs.com/hamhog/p/3560280.html,享受整齐的排版.有效的链接.正确的代码缩进.更好的阅读体验 ...

  3. 九度OJ 1410 垒积木 -- 动态规划

    题目地址:http://ac.jobdu.com/problem.php?pid=1410 题目描述: 给你一些长方体的积木,问按以下规则能最多垒几个积木. 1 一个积木上面最多只能垒另一个积木. 2 ...

  4. Android_Json实例

    概要: 最近由于自己的兴趣,想在Android开发一个自己的App,需要使用服务器,所以交换数据是逃不掉了的,但是学生党没有固定的服务器,因此使用的新浪的SAE,在学习的前期下可以尝试一下,挺不错的一 ...

  5. PHPstrom2016.1激活与汉化【2016.06.21依旧可用】

    : 目前的网络上有很多的关于PHPstrom激活的方法,但是很多都失效了,没有具体的使用日期,这个是我从其他网友那里转载过来的,具体地址忘记了: 方法如下: 需要在联网条件下,打开PHPstrom,在 ...

  6. VS2010类似Eclipse文件查找功能-定位到

    快捷键:Ctrl + , 打开定位到窗口,可以在文件或类文件中查找内容.

  7. jQuery EasyUI之DataGrid使用示例

    jQuery EasyUI是一个轻量级的Web前端开发框架,提供了很多的现成组件帮助程序员减轻前端代码开发量,之前有个项目中就用到了其中的DataGrid. jQuery EasyUI框架的官方主页: ...

  8. CSS弹性盒模型 box-flex

    目前没有浏览器支持boc-flex属性. Firefox支持代替的-moz-box-flex属性 Safari.Opera以及Chrome支持替代的-webkit-box-flex属性 box-fle ...

  9. gentoo下grub文件编辑

    在编译完内核,配置好网络,配置好fstab文件等等,最后一个至关重要的文件要属grub文件了,该文件的配置成功才最终决定gentoo 是否成功装上,首先当然是 emerge grub 了,现在就可以配 ...

  10. 上传头像,界面无跳转,php+js

    上传头像,界面无跳转的方式很多,我用的是加个iframe那种.下面直接上代码. html: //route 为后端接口//upload/avatar 为上传的头像的保存地址//imgurl=/uplo ...