这一章最有价值的条款:26
25.恰当地使用值、(智能)指针或引用传递参数
要区分输入、输出、输入/输出参数,以及值和引用参数。
选择如何传递参数时尽量遵循以下准则:
对于输入参数
1.用const修饰作为输入参数的指针或引用;
2.对于原始类型(如char float)和拷贝开销较小的值对象(如Point,complex<float>),应采用值传递参数
3.对于其他用户定义的类型应采用const引用
4.如果函数需要它的参数的一份拷贝,考虑使用传值而不是引用。这在概念上等同于采用const引用加上一次拷贝,有助于编译器更好地优化掉临时变量。
对于输出或输入/输出参数,
1.如果参数是可选的(调用者可以传递null),或如果函数存储参数的一份拷贝以避免函数操纵参数的所有权,应传递(智能)指针。
2.如果需要参数且函数不存储指向参数的指针以避免影响其所有权。这表明了参数确实是需要的,调用者负责提供正确有效的对象。
不要使用C风格的varargs。
26.保持重载操作符的自然语义
重载操作符必须有很好的理由,并保持其自然语义,如果这很难做到,也许是对重载操作符的误用。
对于值类型(不是所有的类型):"When in doubt, do as the ints do."模仿操作符作用于内建类型时的行为和关系能确保不会产生其他令人惊讶的语义。
程序员希望相关操作符一起定义。如果表达式a@b对于定义的操作符@合法,调用者是否可以写b@a,a@=b?如果操作符可以倒转(如+-*/),两者是否都支持?
有一些很特殊的类库(如分析器生成器,正则表达式引擎等),他们为操作符定义了特定于领域的语义,和C++含义完全不同。
27.尽量使用算术和赋值操作符的标准(canonical)形式
如果定义二元算术操作符,也应提供其赋值操作符定义,这样可以减少重复,提高效率。
通常,对于某些二元操作符,应定义它们的赋值版本,如a@=b和a=a@b,使得他们具有相同含义。其标准方式是根据@=定义@,如下:
T& T::operator@=(const T&){
//实现
return *this;
}
T operator@(const T& lhs, const T& rhs){
T temp(lhs);
return temp@=rhs;
}
注意,这里操作符@不是成员函数,因而它将接受左右参数的隐私类型转换。例如,如果定义了一个类String,有一个构造函数以char为参数,将operator+(const String&, const String&)定义为非成员使得char+String和String+char可以正常工作;而成员函数版本String::operator+(const String&)只接受String+char。、
有可能的话,也将operator@=定义为非成员函数(条款44)。应将所有非成员的操作符置于和T相同的名字空间,便于调用者使用,同时避免名字查找带来的问题(条款57)。
另一种方式是定义操作符@的第一个参数为值参数。通过这种方式,编译器自动进行隐式拷贝,同时这也使得编译器在优化方面有更多余地。
T& operator@=(T& lhs, const T& rhs){
//实现
return lhs;
}
T operator@(T lhs, const T& rhs){
return lhs@=rhs;
}
另一种方式是操作符@返回const值。它的优点在于禁止了一些如a+b=c的不规范的代码,但同时也带来了代价,即使得一些有用的用法,如a=(b+c).replace(pos,n,d),无法使用。
例,字符串+=的实现。
String& String::operator+=(const String& rhs){
//实现
return *this;
}
String operator+(const String& lhs, const String& rhs){
String temp;
temp.Reserve(lhs.size()+rhs.size());
return (temp+=lhs)+=rhs;
}
在某些情况下(如复数的operator*=),操作符可能会对左操作数进行修改,此时通过operator*来实现operator*=更好。
28.尽量使用++和--的标准形式,优先调用前缀形式。
如果不需要原来的值,应使用前缀版本。
以前有个C++的笑话,人们把这种语言称为C++而不是++C是因为它被改进了(incremented),但是很多人仍然把它当C使用(原来的值)。虽然这个笑话已经过时了,但它很能说明两种操作符形式的不同。
应使用前缀形式来实现后缀形式。标准形式如下:
T& T::operator++(){
//增1
return *this;
}
T& T::operator++(int){
T old(*this);
++*this;
return old;
}
在调用的代码中,除非真的必须后缀版本返回的原来的值,尽量使用前缀版本。
29.考虑通过重载来避免隐式类型转换
隐式类型转换提供了句法上的便利(但是参考条款40)。如果创建临时对象是不必要的,而且可以进行优化,应提供正确地匹配参数类型的重载函数。
看一个常见的字符串比较的例子:
class String{
String(const char* txt);
}
bool operator==(const String&, const String&);
//代码某处
if(someString == "Hello"){...}
编译器会把上述比较代码当成if(someString == String("Hello"))来编译。考虑到只需要读取这些字符串而不必进行拷贝,这么转换是相当浪费。解决方法相当简单,重载以避免转换。
bool operator==(const String& lhs, const String& rhs);
bool operator==(const String& lhs, const char* rhs);
bool operator==(const char* lhs, const String& rhs);
30.避免重载&&,||和逗号操作符
编译对内建的&& ||和,有特殊处理,如果对他们重载,他们将成为具有不同语义的普通函数(违反条款26和31).
不重载这三个内建操作符的主要原因是你无法实现他们的完全语义,通常程序员需要这些语义。特别是,他们从左到右求值,&&和||使用short-circuit求值。&&和||先对左表达式求值,如果所得结果已经可以决定结果(&&是false,||是true),右表达式就不再求值。
Employee* e = TryToGetEmployee();
if(e&&e->Manager()){...}
上述代码中,如果e是空,则e->Manager()不会计算。
但是如果&&被重载,则是另外一回事,使用&&的表达式遵循以下函数规则:
1.函数调用在执行函数之前将对所有参数求值
2.参数的执行顺序未定
将上述代码改为使用智能指针:
some_smart_ptr<Employee> e = TryToGetEmployee();
if(e&&e->Manager()){...}
代码将调用重载的&&,代码看上去没有问题,但是在e为空时,有可能调用e->Manager()。
有些代码由于依赖于&&左右两个表达式的求值顺序,导致其他问题。
if(DisplayPrompt()&&GetLine()){...}
如果&&是用户自定义操作符,DisplayPrompt和GetLine哪个先调用没有说明,程序有可能在显示提示之前一直在等待输入。
此类代码在你当前的编译器和build设置可能是可以工作的,但是是脆弱的。编译器可以根据生成代码的大小,可用的寄存器,表达式复杂度等选择它认为最合适的调用顺序。不同的编译器版本,编译器开关设置甚至于函数调用周围的代码都会导致相同的函数调用呈现不同的行为。
逗号操作符也有类似的问题。以下代码中,如果逗号操作符为自定义,则g参数的值是0或1未知。
int i=0;
f(i++),g(i);
表达式模板类库是例外。
31.不要编写依赖于函数参数求值顺序的代码
在使用C的早期,处理器的寄存器是非常珍贵的资源,编译器在为高级语言的复杂表达式有效地分配寄存器方面压力很大。为了生成更快代码,C的创建者给予寄存器分配器相当大的自由度:在调用函数时,参数求值的顺序未指定。对于当前的编译器,这种理由不再成为主要理由,但是参数求值顺序未指定在C++中仍然存在。
考虑以下代码:
void Transmogrify(int, int);
int count=5;
Transmogrify(++count, ++count);
我们能确定的是在进入函数时,count为7,但是哪个参数值为6,哪个为7则是未知。
解决方法很简单-用命名对象来强制求值顺序。如,
int bumped=++count;
Transmogrify(bumped, ++count);