Overloaded Operations and Conversions
(重载操作和转换)
14.1 定义重载操作符(Defining an Overload Operators)
重载操作符必须至少有一个操作数的类型是类。
重载操作符必须至少有一个操作数是类类型和枚举类型。(An overloaded operator must have at least one operand of class or enumeration type.)
重载操作符不会改变原来的优先级和关联关系。(Precedence and Associativity Are Fixed)
短路径求值不再保留(Short-Ciruit Evaluation Is Not Preserved)
这是和内置数据类型不同的。if expression1 && expression2, 对于内置数据类型如果expression1为false,那么expression2就不再判断。但是这个短路径求值方法在操作符重载时不再保留,就是说无论expression1是否为FALSE,都要判断expression1。
类成员对非类成员(Class Member versus Nonmember)
这个命题来自于操作符重载即可以是类成员也可以是非类成员
// member binary operator: left-hand operand bound to implicit this pointer
Sales_item& Sales_item::operator+=(const Sales_item&);
// nonmember binary operator: must declare a parameter for each operand
Sales_item operator+(const Sales_item&, const Sales_item&);
|
它们的区别在于:
类成员返回的是引用,而非类成员返回的是对象。
重载操作符的设计原则
对于类类型对象来说大多数的操作都是没有实际意义的,除非这个类提供了重载操作符。
但是在我们设计类的过程中,不能为了操作符重载而操作符重载。应该先去定义类的接口,然后根据接口提供的操作,把对应的操作转换为操作符重载。(The best way to design operators for a class is first to design the class' public interface. Once the interface is defined, it is possible to think about which operations should be defined as overloaded operators. Those operations with a logical mapping to an operator are good candidates. For example,)
复合赋值操作符(Compound Assignment Operators)
首先说一下子的是啥是符合赋值操作符,Google说,这些都是哈:
+=
-=
*=
/=
%=
&=
^=
|=
<<=
>>=
|
复合赋值操作符(加法)
复合赋值操作符(减法)
复合赋值操作符(乘法)
复合赋值操作符(除法)
复合赋值操作符(取余)
复合赋值操作符(按位与)
复合赋值操作符(按位异或)
复合赋值操作符(按位或)
复合赋值操作符(按位左移)
复合赋值操作符(按位右移)
|
大师给出的原则是:既然你重载了+,那么+=你也要负责哦。(If a class has an arithmetic operator, then it is usually a good idea to provide the corresponding compound-assignment operator as well.)
相等和逻辑关系操作符
- 用作关联容器的键(key)的类应该定义’<’操作符(less than operator)。(Classes that will be used as the key type of an associative container should define the < operator.)
- 如果类的对象要保存在序列容器里,那么一定要为这个类定义等于和小于操作符(’==’, ‘<’)。(Even if the type will be stored only in a sequential container, the class ordinarily should define the equality (==) and less-than (<) operators.)
- 如果类定义了等于操作符,那么还应该定义不等于操作符。(If the class defines the equality operator, it should also define !=.)
- 同样,如果类定义了小于操作符(<),那么它应该定义所有的四种关系操作符(>, >=, <, and <=)。( If the class defines <, then it probably should define all four relational operators (>, >=, <, and <=).)
14.2 输入输出操作符(Input and Output Operators )
重载输出操作符<<
基本的框架结构应该是:
// general skeleton of the overloaded output operator
ostream&
operator <<(ostream& os, const ClassType &object)
{
// any special logic to prepare object
// actual output of members
os << // ...
// return ostream object
return os;
}
|
另外有几点注意事项:
1. IO操作符重载必须是非成员函数。
2. 输出重载应该以最小的方式打印对象的内容。
3. 第一个参数ostream引用,是第二个参数必须是const引用,返回值是ostream引用。
For example:
ostream&
operator<<(ostream& out, const Sales_item& s)
{
out << s.isbn << ""t" << s.units_sold << ""t"
<< s.revenue << ""t" << s.avg_price();
return out;
}
|
其实,我觉得这很像Java中重载tostring()函数。
重载输入操作符>>
For example:
istream&
operator>>(istream& in, Sales_item& s)
{
double price;
in >> s.isbn >> s.units_sold >> price;
// check that the inputs succeeded
if (in)
s.revenue = s.units_sold * price;
else
s = Sales_item(); // input failed: reset object to default state
return in;
}
|
有几点注意事项:
- 返回值是istream的引用。
- 第二个参数不能是const,因为读入的数据就是写入到这个对象里的。
- 对输入有效性的判断是在使用读入的数据之前进行的:if (in)。
进一步说,在读入数据时,我们还有可能需要进行数据有效性的判断。那么How to do?简单地说,就是要在重载的输入操作符中增加有效性的判断,如果输入的数据不满足有效性,那么就对istream的failbit置位。同样的道理,也可以处理badbit,eofbit。(Usually an input operator needs to set only the failbit. Setting eofbit would imply that the file was exhausted, and setting badbit would indicate that the stream was corrupted. These errors are best left to the IO library itself to indicate.)
14.3 Arithmetic and Relational Operators
算术操作符
- 为了和内置操作保持一致,重载的加法操作返回的右值对象。(Note that to be consistent with the built-in operator, addition returns an rvalue, not a reference.)
- 定义算术运算符和相对应的复合操作符的类应该通过使用复合操作符来实现算数操作符。(Classes that define both an arithmetic operator and the related compound assignment ordinarily ought to implement the arithmetic operator by using the compound assignment.)注意这个顺序是不能反的,也就是不能用算术运算符去实现复合操作符。很容易理解,因为算术操作符返回的是对象,而复合操作符返回的*this。
相等操作符
- 定义等于操作符(==)就要同时定义不等于操作符(!=)。二者之间一个完成实际的工作,另一个仅仅是调用前者。
- 如果类定义了等于操作符(==),那么就更加容易利用标准库的算法了。
关系操作符
关系操作符是指:<, >。
要注意如果定义关系操作符,要保证它们和等于操作符不冲突。如果冲突,那就要有取舍,大师给的Sales_item的例子,就无需定义关系操作符,为了避免和等于操作符冲突。P520的说明仔细琢磨后,有点意思。
14.4 Assignment Operators
l 赋值操作符必须是成员函数。(every assignment operator, regardless of parameter type, must be defined as a member function.)
l 赋值操作符可以重载
class string {
public:
string& operator=(const string &); // s1 = s2;
string& operator=(const char *); // s1 = "str";
string& operator=(char); // s1 = 'c';
// ....
};
|
正是因为string对赋值操作符进行了重载,下面的赋值才都是有效的:
string car ("Volks");
car = "Studebaker"; // string = const char*
string model;
model = 'T'; // string = char
|
l 赋值应该返回*this的引用。
14.5 下标操作符(Subscript Operator )
(string和vector是最好的例子。)
l 下标操作符必须是类成员函数。
l 需要定义下标操作符的类要定义两个版本的下标操作,一个是非const成员,返回值是引用;另一个是const成员,返回值是const引用。(a class that defines subscript needs to define two versions: one that is a nonconst member and returns a reference and one that is a const member and returns a const reference.)
14.6 MemberAccess Operators
是指:解引用(*)和箭头( ->)操作符。它们主要是用在实现智能指针(smart pointer)。
先说说这两个抓狂的操作符
我是这么掰持的:
箭头实际上是*.操作。什么意思?(*ptr).fun()对应的就是ptr->fun(),由于’.’操作符的优先级高于解引用*,所以->的优先级也应高于解引用。所以*ptr->sp实际上就是*(ptr->sp)。
再看看大师给的这个经典的smart pointer的例子
纠结。
class ScrPtr {
friend class ScreenPtr;
Screen *sp;
size_t use;
ScrPtr(Screen *p): sp(p), use(1) { }
~ScrPtr() { delete sp; }
};
|
/*
* smart pointer: Users pass to a pointer to a dynamically allocated Screen, which
* is automatically destroyed when the last ScreenPtr goes away
*/
class ScreenPtr {
public:
// no default constructor: ScreenPtrs must be bound to an object
ScreenPtr(Screen *p): ptr(new ScrPtr(p)) { }
// copy members and increment the use count
ScreenPtr(const ScreenPtr &orig):
ptr(orig.ptr) { ++ptr->use; }
ScreenPtr& operator=(const ScreenPtr&);
// if use count goes to zero, delete the ScrPtr object
~ScreenPtr() { if (--ptr->use == 0) delete ptr; }
// 以下是定义的解引用和箭头操作
Screen &operator*() { return *ptr->sp; } //返回的是Screen对象的引用
Screen *operator->() { return ptr->sp; } //返回的是指向Screen对象的指针
const Screen &operator*() const { return *ptr->sp; }
const Screen *operator->() const { return ptr->sp; }
private:
ScrPtr *ptr; // points to use-counted ScrPtr class
};
|
对于解引用操作,那是好理解滴。
纠结在箭头操作上了L
箭头可以看做是二元操作符。它的两个操作数是对象以及函数名称。为了获得成员接引用对象。即使这样,箭头操作符不需要显式的形参。(Operator arrow is unusual. It may appear to be a binary operator that takes an object and a member name, dereferencing the object in order to fetch the member. Despite appearances, the arrow operator takes no explicit parameter.)
我们可以把右边的操作数看做是是标识符,它对于类的一个成员。(the right-hand operand is an identifier that corresponds to a member of a class.)
KAO,那怎么理解
- 首先根据优先级,要把它看做是:(point->action) ();
- point是对象,可以看做是point.operator->()->action()。(这个问题让我想的很纠结,人笨起来,挡都挡不住。以至于中午在SOGO都还在想这个问题,然后忽的一下醒悟,幸福。)operator->()可以简单的看做是对象成员,返回值是指针,然后执行->action()部分,这里的->则是地地道道的箭头操作符了。
对箭头重载返回值的约束条件
返回值要么是类类型的指针,或者是类类型的对象,不过这个类要定义了它自己的箭头操作符。(The overloaded arrow operator must return either a pointer to a class type or an object of a class type that defines its own operator arrow.)
14.7 自增和自减操作符Increment and Decrement Operators
- 首先这两个操作符要是类的成员。
- 其次为了和内置的操作保持一致。前缀操作符应该返回的是引用,这个引用是对应到增加或减少后的对象。
- 为了区分前缀和后缀操作,后缀操作符重载包含一个额外的int类型的形参。
定义后自增/自减操作符
要区分前自增/自减和后自增/自减。区分的方法是后自增/自减要包含有一个int类型的形参。但是这个int类型的形参并不会被使用。仅仅是为了和前自增/自减加以区分。
// postfix: increment/decrement object but return unchanged value
CheckedPtr CheckedPtr::operator++(int)
{
// no check needed here, the call to prefix increment will do the check
CheckedPtr ret(*this); // save current value
++*this; // advance one element, checking the increment
return ret; // return saved state
}
|
返回值
对比前自增的返回值是自增后的对象的引用,后自增返回值是没有自增的对象,而不是引用。
显式调用自增
CheckedPtr parr(ia, ia + size); // iapoints to an array of ints
parr.operator++(0); // call postfix operator++
parr.operator++(); // call prefix operator++
|
14.8 Call Operator and Function Objects
调用操作符是指:operator()
基本定义方法:
struct absInt {
int operator() (int val) {
return val < 0 ? -val : val;
}
};
|
调用:
int i = -42;
absInt absObj; // object that defines function call operator
unsigned int ui = absObj(i); // calls absInt::operator(int)
|
函数调用操作符必须定义为成员函数。一个类可以定义多个版本的调用操作符,每个调用操作符定义的形参的类型和数量不同。
如果看上面的这个用法的例子,实在看不出调用操作符有啥优点,但是如果结合类库算法来使用,就能够看出调用操作符的灵活性。
定义:
// determine whether a length of a given word is longer than a stored bound
class GT_cls {
public:
GT_cls(size_t val = 0): bound(val) { }
bool operator()(const string &s)
{ return s.size() >= bound; }
private:
std::string::size_type bound;
};
|
调用:
for (size_t i = 0; i != 11; ++i)
cout << count_if(words.begin(), words.end(),GT_cls (i))
<< " words " << i
<< " characters or longer" << endl;
|
这,要是用函数定义来写,就不得不写11个不同的函数,而这些函数仅仅是要判断的长度不同。冗余!如果这样写,简直就是纺织女工写的程序。
标准库定义的函数对象
每个标准库函数对象实际上都代表了一种操作,并且它们还是模板:o 模板类型代表了操作数的类型。(The Template Type Represents the Operand(s) Type)
plus<Type>
minus<Type>
multiplies<Type>
divides<Type>
modulus<Type>
negate<Type>
|
applies +
applies -
applies *
applies /
applies %
applies -
|
equal_to<Type>
not_equal_to<Type>
greater<Type>
greater_equal<Type>
less<Type>
less_equal<Type>
|
applies ==
applies !=
applies >
applies >=
applies <
applies <=
|
logical_and<Type>
logical_or<Type>
logical_not<Type>
|
applies &&
applies |
applies !
|
一般的运算
plus<int> intAdd; // function object that can add two int values
negate<int> intNegate; // function object that can negate an int value
// uses intAdd::operator(int, int) to add 10 and 20
int sum = intAdd(10, 20); // sum = 30
// uses intNegate::operator(int) to generate -10 as second parameter
// to intAdd::operator(int, int)
sum = intAdd(10, intNegate(10)); // sum = 0
|
除了一般的运算,这些标准库定义的库函数还可以用在算法调用中使用。
// passes temporary function object that applies > operator to two strings
sort(svec.begin(), svec.end(), greater<string>());
|
函数对象的函数适配器
为啥要用适配器?
适配器有两类:
1) Binders:绑定器。把二元函数对象转换为一元的函数对象
又分成:bind1st, bind2nd.
2) Negators:取反器。函数对象的真值取反
又分成:not1, not2.
14.9 Conversions and Class Types
为什么转换有用?
知了J
问题的提出是这样的,如果我们需要定义一个SmartInt类,这个类的值被限制在0-255之间,也就是无符号字符(unsigned char),同时这个类还要支持所有的算术运算。如果没有类型转换,意味着我们要定义48个操作符。L
但是很幸运,C++提供了类型转换机制,类定义应用在自己对象上的转换规则。
SmallInt si(3);
si + 3.14159; // 先把si转换成int,再由int转换成double,前者是类类型转换,后者是标准类型转换
|
转换操作符
定义
转换操作符必须是成员函数。它定义的是从一种类类型值转换成其它类型的值。(A conversion operator is a special kind of class member function. It defines a conversion that converts a value of a class type to a value of some other type.)
class SmallInt {
public:
SmallInt(int i = 0): val(i)
{ if (i < 0 || i > 255)
throw std::out_of_range("Bad SmallInt initializer");
}
operator int() const { return val; }
private:
std::size_t val;
};
|
一般形式:
可以看出转换操作符是没有形参的,另外也不定义返回值的类型。(The function may not specify a return type, and the parameter list must be empty.)
这里的type即可是内置类型,也可以是类类型,甚至还可以是由typedef定义的名称。(type represents the name of a built-in type, a class type, or a name defined by a typedef.)
另外转换操作符强调的是转换,它并不改变对象的值,因此转换操作符一般都定义成const成员。例如上面的定义:operator int() const { return val; }
转换类型
但是转换类型也是有限制的,
首先void是不行的,数组(array)或者函数类型(function type)也是不可以的。
其次指针是可以的,无论是指向数据的还是指向函数的,都是可以的。
第三引用类型也是可以的。
类类型转换只能应用一次(Class-Type Conversions and Standard Conversions)
类类型转换后面只能跟标准类型转换,而不能再跟类类型转换了。(A class-type conversion may not be followed by another class-type conversion.)
纠结了L :
大师给了这个定义,注意si这个对象的创建过程是先把intVal通过类类型转换为SmallInt,再调用拷贝构造函数而生成的。
但是如果SmallInt里定义了形参是Integral的构造函数,那SmallInt si(intVal);调用的是哪个?L
int calc(int);
Integral intVal;
SmallInt si(intVal); // ok: convert intVal to SmallInt and copy to si
|
标准类型转换优先于类类型转换
因此下面的定义是有效的:
void calc(SmallInt);
short sobj;
// sobj 先由short 转换为 int,这是标准类型转换
// int通过 SmallInt(int) 构造函数转换为 SmallInt ,这是使用构造函数来执行隐式转换
calc(sobj);
|
实参匹配和转换
(这部分其实在第一次阅读的时候可以跳过。因为都属于advance类型的主题,比较纠结。今天北京又是一个高温无雨,不过湿度有点大了,可怕的桑拿天L读这部分。)
类类型转换在带来便利的同时也是编译错误的源泉。用起来要格外的小心。之所以会是这样,是由于有多种方式来实现一个类型到另一个类型的转换。(Problems arise when there are multiple ways to convert from one type to another.)
实参匹配和多种转换操作符
一般情况下,类提供到两种内置类型的转换,或者转换到两种内置类型的做法都是错误的。(Ordinarily it is a bad idea to give a class conversions to or from two built-in types.)例如Smallint类提供了int,还提供了double,那么下面的代码就会让编译器很纠结:
void extended_compute(long double);
SmallInt si;
extended_compute(si); // error: ambiguous
|
si可以转换成int和double,然后呢,都可以通过标准类型转换转换为long double,完了,编译器不知道该用那个了。
结论就是:
如果在调用时可以用到两个转换操作符,那么如果存在标准转换等级时,那么跟随转换操作符的标准转换等级就是用来确定最佳匹配的依据。(If two conversion operators could be used in a call, then the rank of the standard conversion, if any, following the conversion function is used to select the best match.
)
实参匹配和构造函数转换
例如Smallint类提供了int,还提供了double类型的构造函数,那么下面的代码就会产生二义性:
void manip(const SmallInt &);
double d; int i; long l;
manip(l); // error: ambiguous
|
l有两种方式来处理:
- 先标准转换(long->double),再调用构造函数SmallInt(double)。
- 先标准转换(long->int)),再调用构造函数SmallInt(int)。
因此编译器就会报错了。
结论:
如果类定义有两个构造函数的类型转换,标准转换的级别(如果存在)用来判断最佳匹配。(When two constructor-defined conversions could be used, the rank of the standard conversion, if any, required on the constructor argument is used to select the best match.)
当两个类都定义转换引起二义性
一个类定义的是构造函数的类型转换,另一个类是类型转换操作(conversion operation):
class Integral;
class SmallInt {
public:
SmallInt(Integral); // convert from Integral to SmallInt
// ...
};
class Integral {
public:
operator SmallInt() const; // convert from SmallInt to Integral
// ...
};
void compute(SmallInt);
Integral int_val;
compute(int_val); // error: ambiguous
|
int_val这个Integral类型如何转换为SmallInt呢?因为有两种个方法,编译器是不会选择的。
解决办法就是显式调用:
compute(int_val.operator SmallInt()); // ok: use conversion operator
compute(SmallInt(int_val)); // ok: use SmallInt constructor
|
结论:
最好的方法避免二义性是避免一对类都提供彼此间的隐式转换。(The best way to avoid ambiguities or surprises is to avoid writing pairs of classes where each offers an implicit conversion to the other.)
重载确定类实参(Overload Resolution and Class Arguments)
标准转换跟随转换操作符
哪个函数最匹配取决于在匹配的不同函数里是否包含一个或多个类类型转换。(Which function is the best match can depend on whether one or more class-type conversions are involved in matching different functions.)
如果两个转换序列使用同样的转换操作,那么跟着类类型转换的标准类型转换作为选择标准(The standard conversion sequence following a class-type conversion is used as a selection criterion only if the two conversion sequences use the same conversion operation.)
很难理解的概念,但是一看例子就了然了J
void compute(int);
void compute(double);
void compute(long double);
SmallInt si;
compute(si);
|
调用的是void compute(int);因为是精确匹配。
多个转换和重载确定
如果类类型里定义了两个内置类型的转换,例如SmallInt定义有转换操作int()和double(),那么还是上面的例子compute(si);编译器就会报错,因为存在两种调用方式:
void compute(int);
void compute(double);
编译器傻了。
解决办法:为了避免二义性而显式强制转换(Explicit Constructor Call to Disambiguate)
SmallInt si;
compute(static_cast<int>(si)); // ok: convert and call compute(int)
|
标准转换和构造函数
两个类都定义了相同类型的构造函数:
class SmallInt {
public:
SmallInt(int = 0);
};
class Integral {
public:
Integral(int = 0);
};
void manip(const Integral&);
void manip(const SmallInt&);
manip(10); // error: ambiguous
|
解决方法就是显式调用构造函数:
manip(SmallInt(10)); // ok: call manip(SmallInt)
manip(Integral(10)); // ok: call manip(Integral)
|
另外,即使SmallInt中定义的类型转换是short,也是没意义的,编译器依然会报错。事实上,当选择重载版本的调用时,一个调用标准类型转换,另一个不需要,这无关紧要。( The fact that one call requires a standard conversion and the other does not is immaterial when selecting among overloaded versions of a call.)编译器必不是更喜欢直接的构造函数。
重载,转换和操作符(Overloading, Conversions, and Operators)
这个问题的提出是这样滴:
ClassX sc;
int iobj = sc + 3;
|
这样一个语句,你说这是操作符重载,或者是类类型转换(构造函数,转换操作符)?
重载确定和操作符
成员函数和非成员函数都是有可能的这一事实改变选择候选函数的方式。(The fact that member and nonmember functions are possible changes how the set of candidate functions is selected.)
操作符的候选函数
根据表达式中用到的操作符,候选函数包括内置的操作符以及所有一般的非成员函数版本的操作符。除此之外,如果左边的操作数是类类型,那么候选函数还应包括类定义的重载操作符。(In the case of an operator used in an expression, the candidate functions include the built-in versions of the operator along with all the ordinary nonmember versions of that operator. In addition, if the left-hand operand has class type, then the candidate set will contain the overloaded versions of the operator, if any, defined by that class.)
调用自己决定要考虑的名字的范围(the call itself determines the scope of names that will be considered.)如果调用是通过类对象来完成的,那么只会考虑类的成员函数。
转换能够导致内置操作符的二义性(Conversions Can Cause Ambiguity with Built-In Operators)
同一个类即提供到算术类型的转换函数又重载操作符,就会导致重载操作符和内置操作符的二义性。
这样的例子就是SmallInt即定义到int的转换函数,又重载操作符’+’,那么int i = s3 + 0;就会产生二义性的错误,是把s3转换成int再去计算,还是把0通过构造函数转换成SmallInt再去计算呢?
class SmallInt {
public:
SmallInt(int = 0); // convert from int to SmallInt
// conversion to int from SmallInt
operator int() const { return val; }
// arithmetic operators
friend SmallInt
operator+(const SmallInt&, const SmallInt&);
private:
std::size_t val;
};
SmallInt s3 = s1 + s2; // ok: uses overloaded operator+
int i = s3 + 0; // error: ambiguous
|
规则(rules of thumb)
针对类进行重载操作符(overloaded operators)、构造函数转换(conversion constructors)以及转换函数(conversion functions)设计必须小心。构造函数转换(conversion constructors)以及转换函数(conversion functions)又可以统称为转换操作(conversion operators)。如果类既定义了转换操作又重载操作符,那么很容易产生二义性。(ambiguities are easy to generate if a class defines both conversion operators and overloaded operators.)这里有两条规则:
1. 不要定义相互转换类。(Never define mutually converting classe)
2. 避免定义转换到内置算术类型的转换函数。(Avoid conversions to the built-in arithmetic types.)特别的,如果定义了这类函数,那么
l 不要重载以算术类型为形参的操作符。(Do not define overloaded versions of the operators that take arithmetic types.)
l 不要定义超过一种的转换到算术类型的转换函数。(Do not define a conversion to more than one arithmetic type.)如果定义了到int的转换函数就不要定义转换到double的转换函数。