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

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. Hibernate+struts+JqueryAjax+jSON实现无刷新三级联动

    看网上JqueryAjax三级联动的例子讲不是很全,代码也给的不是很全,给初学者带来一定的难度.小弟自己写了一个,可能有些地方不是很好,希望大家能够提出建议. 用的是Hibernate+struts2 ...

  2. NOIP200701

    题是这样的: 试题描述 某小学最近得到了一笔赞助,打算拿出其中一部分为学习成绩优秀的前5名学生发奖学金.期末,每个学生都有3门课的成绩:语文.数学.英语.先按总分从高到低排序,如果两个同学总分相同,再 ...

  3. Android_时间服务

    接着上一节,这次我查看了Android的时间服务,觉得帮助很大,解决了我很多疑问,下面我就自己总结了一下,加深了自己的印象,好记性不如烂笔头,还真讲得很不错,收下吧?看下图如何利用线程更新UI组件 重 ...

  4. centos svn安装

    http://fengjunoo.iteye.com/blog/1759265(参考) 以前在ubuntu上安装过一次svn,那次弄得有些麻烦. 这次记录下centos环境下安装svn的步骤 其实简单 ...

  5. 用Java开发一个本地服务管理软件

    一.最终界面先贴上最终效果图,图1为初始化界面,图二为点击启动/停止之后的中间过渡状态,图三为启动成功后弹出的提示框 把动态gif图片嵌入到jpg背景图中?用Adobe ImageReady即可办到 ...

  6. npm:Node.js的软件包管理器

    npm https://www.npmjs.com/ 2016-08-03

  7. MySQL复制(三) --- 高可用性和复制

    实现高可用性的原则很简单: 冗余(Redundancy):如果一个组件出现故障,必须有一个备用组件.这个备用组件可以是standing by的,也可以是当前系统部署中的一部分. 应急计划(Contig ...

  8. C# 实体model验证输出

    新建Model实体: [Required(ErrorMessage = @"地址 1 为必填项!")] [StringLength(, ErrorMessage = @" ...

  9. sql触发器知识

    触发器中的Inserted和deleted临时表: SQL2000中,inserted表和deleted表用于存放对表中数据行的修改信息.他们是触发器执行时自动创建的,放在内存中,是临时表.当触发器工 ...

  10. ssh中使用set的地方及ref

    22:30 2014/5/2 1.层与层间的set:  xml方式:在action中曾有service的set方法,service层要有dao的service的set方法,dao曾要有hibernat ...