[译]GotW #4 Class Mechanics
你对写一个类的细节有多在行?这条款不仅注重公然的错误,更多的是一种专业的风格。了解这些原则将会帮助你设计易于使用和易于管理的类。
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的更多相关文章
- [译]GotW #89 Smart Pointers
There's a lot to love about standard smart pointers in general, and unique_ptr in particular. Proble ...
- [译]GotW #6b Const-Correctness, Part 2
const和mutable对于书写安全代码来说是个很有利的工具,坚持使用它们. Problem Guru Question 在下面代码中,在只要合适的情况下,对const进行增加和删除(包括 ...
- [译]GotW #6a: Const-Correctness, Part 1
const 和 mutable在C++存在已经很多年了,对于如今的这两个关键字你了解多少? Problem JG Question 1. 什么是“共享变量”? Guru Question 2. con ...
- [译]GotW #3: Using the Standard Library (or, Temporaries Revisited)
高效的代码重用是良好的软件工程中重要的一部分.为了演示如何更好地通过使用标准库算法而不是手工编写,我们再次考虑先前的问题.演示通过简单利用标准库中已有的算法来避免的一些问题. Problem JG Q ...
- [译]GotW #2: Temporary Objects
不必要的和(或)临时的变量经常是罪魁祸首,它让你在程序性能方面的努力功亏一篑.如何才能识别出它们然后避免它们呢? Problem JG Question: 1. 什么是临时变量? Guru Q ...
- [译]GotW #1: Variable Initialization
原文地址:http://herbsutter.com/2013/05/09/gotw-1-solution/ 第一个问题强调的是要明白自己在写什么的重要性.下面有几行简单的代码--它们大多数之间都有区 ...
- [译]GotW #5:Overriding Virtual Functions
虚函数是一个很基本的特性,但是它们偶尔会隐藏在很微妙的地方,然后等着你.如果你能回答下面的问题,那么你已经完全了解了它,你不太能浪费太多时间去调试类似下面的问题. Problem JG Ques ...
- [译]GotW #1: Variable Initialization 续
Answer 2. 下面每行代码都做了什么? 在Q2中,我们创建了一个vector<int>且传了参数10和20到构造函数中,第一种情况下(10,20),第二种情况是{10, 20}. 它 ...
- Go开发中的十大常见陷阱[译]
原文: The Top 10 Most Common Mistakes I've Seen in Go Projects 作者: Teiva Harsanyi 译者: Simon Ma 我在Go开发中 ...
随机推荐
- PHP 实现对象的持久层,数据库使用MySQL
http://www.xuebuyuan.com/1236808.html 心血来潮,做了一下PHP的对象到数据库的简单持久层. 不常用PHP,对PHP也不熟,关于PHP反射的大部分内容都是现学的. ...
- java多线程总结二:后台线程(守护线程)
所谓的后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分.因此当所有的非后台线程结束时,程序也就终止了,同时会杀死所有后台线程.反过来说,只要有任何非 ...
- Oracle笔记(三)单行函数
-函数 函数像一个黑盒子一样(看不到里边的构造),有参数返回值,可以为我们完成一定的功能. -单行 这种函数会对结果中的每一行计算一次,每行返回一个结果,单行概念区别于分组函数. 单行函数主要分为以下 ...
- (四)JAVA使用POI操作excel
1,字体处理 Demo12.java package com.wishwzp.poi; import java.io.FileOutputStream; import org.apache.poi.h ...
- 第五篇、HTML标签类型
<!--1.块级标签 独占一行,可以设置高度和宽度 如:div p h ul li -----display: none(隐藏标签) block(让行内标签变块级标签) inline(让块级标 ...
- CocoaPods的使用详解
CocoaPods是什么 当我们开发 iOS 项目时候,会经常使用到第三方类库,并且会使用很多.大家的做法基本上都是到 GitHub 上下载一个一个的类库,然后导入到工程中,并且引入各种的类库,做各种 ...
- 【转】如何编译安装PHP扩展
本文参考 一开始安装PHP的时候,我们并不知道需要哪些扩展,所以只有等到我们真正用到的时候才想办法去安装. 安装PHP扩展最简单的办法就是 sudo apt-get install php5-xxx ...
- text与button上下不对齐解决方法
火狐可以对齐,但是IE8不行,加上浮动就可以了 .search_right input[type=button] { float:right; }
- demo_01 css3中的radius
css属性:border-radius :border:边框:radius:弧度:所以这个属性的意思很明了. 下面实现一个小demo: <!doctype html> <html&g ...
- 加解密算法二:非对称加解密及RSA算法的实现
加密和解密使用不同的密钥的一类加密算法.这类加密算法通常有两个密钥A和B,使用密钥A加密数据得到的密文,只有密钥B可以进行解密操作(即使密钥A也无法解密):相反,使用密钥B加密数据得到的密文,只有密钥 ...