第 14 章 重载运算与类型转换
14.1 基本概念
重载的运算符是具有特殊名字的函数。
重载运算符的参数数量与该运算符作用的运算对象数量一样多。除了重载的函数调用运算符 operator() 之外其他重载运算符不能含有默认实参。
一个重载运算符函数是成员函数时,左侧运算对象绑定到隐式的 this 指针上,此时显式的参数个数要比运算符的对象数少 1。
对于一个运算符函数,它至少有一个类类型的参数。(不能重载作用于内置类型的运算符)。
我们只能重载现有的运算符,无法发明新的运算符。
不能改变现有运算符的优先级和结合律。
可以重载的运算符
+ - * / % ^
& | ~ ! , =
< > <= >= ++ --
<< >> == != && ||
+= -= /= %= ^= &=
|= *= <<= >>= [] ()
-> ->* new new[] delete delete[]
不能重载的运算符
::
.*
.
?:
我们可以像调用普通函数一样调用重载运算符
operator+(a, b);
a + b; // 两种形式等价
a.operator+=(b);
a += b;
某些算符不应该被重载
因为重载运算符本质上是函数调用,因此无法保留求职顺序的规则。例如逻辑运算符的短路特性。因此不建议重载逻辑运算符,同时不建议重载逗号运算符和取地址运算符。
选择作为成员或者非成员
赋值 =
、下标 []
、调用 ()
和成员访问箭头 ->
必须是成员。
复合赋值运算符一般来说应该是成员,但并非必须。
改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符通常应该是成员。
具有对称性的运算符可能转换任意一段的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。如果运算对象时混合类型的则需要定义为非成员函数。
14.2 输入和输出运算符
输出类运算符尽量不要考虑格式化操作。
输入输出运算符必须是非成员函数。
#include <iostream>
#include <string>
using namespace std;
struct A {
int i;
string s;
};
istream& operator>>(istream &is, A &a) {
is >> a.i >> a.s;
if (is) {
; // check input state
} else {
cerr << "read A failed\n";
}
return is;
}
ostream& operator<<(ostream &os, const A &a) {
cout << "(" << a.i << "," << a.s << ")";
return os;
}
int main() {
A a;
cin >> a;
cout << a;
return 0;
}
输入需要检查读入是否成功,输出则不需要。
标示错误
一般来说即便输入是成功的,但数据可能依旧不合法(针对具体的功能),输入运算符需要检测数据是否符合设计。此时可以设置流状态来标示失败信息。可以设置 failbit、eofbit 或者 badbit。
14.3 算术和关系运算符
如果类同时定义了算术运算符和相关的复合赋值运算符,则通常情况应该使用赋值来实现算术运算符。
14.3.1 相等运算符
==
应该具有传递性。
定义了 ==
则也应该定义 !=
并且应该把其中一个的实现委托给另一个。
14.3.2 关系运算符
注意 <
的定义应当与 ==
一致。
14.5 下标运算符
通常下标运算符会有两个版本,返回普通引用、作为常量成员返回常量引用。
#include <iostream>
using namespace std;
struct A {
int* a;
A(int n) {
a = new int[n];
for (int i = 0; i < n; i++) a[i] = 0;
}
int& operator[](int i) { return a[i]; }
const int& operator[](int i) const { return a[i]; }
};
int main() {
A a(10);
a[5] = 3;
cout << a[5] << "\n";
a[5] = 5;
cout << a[5] << "\n";
const A b(3);
cout << b[0] << "\n";
return 0;
}
14.6 递增和递减运算符
前置版本应该返回递增或递减后对象的引用。
为了区分前置和后置运算符,后置版本将接受一个额外的(不被使用的)int 类型的形参。编译器会为这个形参提供一个值为 0 的实参。尽管语法上可以使用这个额外的形参,但是实际通常不会这么做。显式调用时需要给出这个参数来调用后置运算符。
class A {
A& operator++(); // 前置
A operator++(int); // 后置
};
A a;
a.operator++();
a.operator++(0);
可以把后置运算符的定义委托给前置运算符。
14.7 成员访问运算符
#include <iostream>
using namespace std;
struct A {
int i;
A(int i) : i(i) {}
void func() { cout << "a function\n"; }
};
struct AP {
A &a;
AP(A &a) : a(a) {}
A& operator*() const {
return a;
}
A* operator->() const {
return &a;
}
};
struct APP {
AP &p;
APP(AP &p) : p(p) {}
AP& operator*() const {
return p;
}
AP& operator->() const {
return p;
}
};
int main() {
A a(10);
AP p(a);
cout << (*p).i << endl;
cout << p->i << endl;
(*p).func();
p->func();
APP pp(p);
cout << (**pp).i <<endl;
cout << (*pp)->i << endl;
cout << pp->i << endl;
return 0;
}
重载的 operator*
不一定是类的成员,也可以自己定义一些操作。
箭头运算符则不能改变获取成员这个事实。
形如 point->mum
的表达式,point 必须是指向类对象的指针或者是一个重载了 operator-> 的类。它将分别等价于以下形式
(*point).mem; // point 是指针
point.operator->()->mem; // point 是类
这也解释了上面程序中 pp->i
会链式调用的原因。
14.8 函数调用运算符
如果类定义了调用运算符,则该类的对象称作函数对象 function object
。
含有状态的函数对象类
因为作为对象可以保存一些状态,因此函数对象使用起来比函数更灵活。
泛型对象也经常作为泛型算法的实参。
#include <iostream>
#include <algorithm>
using namespace std;
class PrintString {
public:
PrintString(ostream &o = cout, char c = ' ')
: os(o), sep(c) {}
void operator()(const string &s) const { os << s << sep; }
private:
ostream &os;
char sep;
};
int main() {
vector<string> vec = {"1", "3", "5", "7", "9"};
for_each(vec.begin(), vec.end(), PrintString(cerr, '\n'));
return 0;
}
14.8.1 lambda 是函数对象
编译器会将 lambda 翻译成一个未命名类的未命名对象。在 lambda 表达式产生的类中含有一个重载的函数调用运算符,它的形参列表和函数体与 lambda 表达式完全一样。默认情况下 lambda 不能改变它捕获的变量,在此情况下,该函数调用运算符是一个 const 成员函数。如果 lambda 被声明为可变的,则调用运算符就不是 const 的了。
通过引用捕获变量时,由成语确保 lambda 执行时引用所引的对象确实存在。编译器可以直接使用该引用而无须在 lambda 产生的类中将其储存为数据成员。
通过值捕获的变量被拷贝到 lambda 中。lambda 产生的类必须为每个值捕获的变量建立数据成员,同时创建构造函数,其使用捕获的变量的值来初始化数据成员。
lambda 表达式产生的类不含默认构造函数、赋值运算符及默认析构函数;是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定。
14.8.2 标准库定义的函数对象
在 functional 头文件中
plus<T> minus<T> multiplies<T> divides<T> modulus<T> negate<T>
equal_to<T> not_equal_to<T> greater<T> greater_equal<T>
less<T> less_equal<T>
logical_and<T> logical_or<T> logical_not<T>
特别的,标准库规定其函数对象对于指针同样适用。
vector<string *> vec;
// sort(vec.begin(), vec.end(), [](string *a, string *b){
// return a < b; }); // 未定义行为
sort(vec.begin(), vec.end(), less<string *>());
14.8.3 可调用对象与 function
C++ 中有几种可调用的对象:函数、函数指针、lambda 表达式、bind 创建的对象以及重载了函数调用运算符的类对象。
和其他对象一样,可调用的对象也有类型。lambda 有它自己唯一的类类型。函数及函数指针的类型由返回值类型和实参类型决定。
两个不同类型的可调用对象可能共享同一种调用形式 call signature
。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型。
有时我们可以使用函数表 function table
来存储指向可调用对象的“指针”。
如果我们使用如下形式,则只能存储函数指针
map<string, int(*)(int,int)> binops;
标准库 function 类型
function 定义在 functional 头文件中。
function<T> f; // T 是 retType(args)
function<T> f(nullptr);
function<T> f(obj); // obj 可以是任何的可调用对象
f; // 返回 f 是否为空
f(args); // 调用 f
function<T>::result_type;
function<T>::argument_type;
function<T>::first_argument_type;
function<T>::second_argument_type;
重载函数与 function
注意重载函数我们无法直接用函数名初始化 function (二义性)。这时我们可以通过函数指针或套一层 lambda 表达式来传入想要的重载函数。
14.9 重载、类型转换与运算符
转换构造函数和类型转换运算符共同定义了类类型转换 class type conversions
,也称用户定义的类型转换 user-defined conversions
。
14.9.1 类型转换运算符
类型转换运算符 conversion operator
是类的一种特殊成员函数,它负责将一个类类型的值准换成其他类型。
operator type() const;
必须是成员函数,不能声明返回类型,形参列表也必须为空,通常应该是 const 的。
定义含有类型转换运算符的类
class SmallInt {
public:
SmallInt(int i = 0) : val(i) {
if (i < 0 || i > 255)
throw std::out_of_range("Bad SmallInt value");
}
operator int() const { return val; }
private:
std::size_t val;
};
类型转换运算符可能产生意外结果
实践中很少提供类型转换运算符。在大多数情况下,如果类型转换自动发生,用户可能会感觉比较意外,而不是感受到了帮助。
int i = 42;
cin << i;
例如这里 cin 本身没有定义 <<
算符,如果隐式转换存在,这里就可以通过编译,而不是是发生错误。
显式的类型转换运算符
C++11 中引入了显式的类型转换运算符 explicit conversion operator
explicit operator int() const { return val; }
编译器通常不会将一个显式的类型转换运算符用于隐式类型转换。除非表达式被用作条件,显式类型转换将被隐式的执行:
if、while 及 do 语句的条件部分
for 语句头的条件表达式
逻辑非运算符 !
、逻辑或运算符 ||
、逻辑与运算符 &&
的运算对象
条件运算符 ?:
的条件表达式
14.9.2 避免有二义性的类型转换
两种情况下可能出现多重转换路径。第一种情况是两个类提供相同的类型转换:例如,当 A 类定义了一个接受 B 类对象的转换构造函数,同时 B 类定义了一个转换目标是 A 类的类型转换运算符时。
第二种情况是类定义了多个转换规则。
实参匹配和相同的类型转换
struct B;
struct A {
A() = default;
A(const B&);
};
struct B {
operator A() const;
};
A f(const A&);
B b;
// A a = f(b); // f(B::operator A()) ? f(A::A(const B&)) ?
这时可以使用显示的调用方式
A a1 = f(b.operator A());
A a2 = f(A(b));
注意无法用强制类型转换来解决二义性问题,因为强制类型转换本身也面临二义性。
二义性与转换目标为内置类型的多重类型转换
struct A {
A(int = 0);
A(double);
operator int() const;
operator double() const;
};
void f(long double);
A a;
// f(a); // f(A::operator int()) ? f(A::operator double()) ?
long lg;
// A a2(lg); // A::A(int) ? A::A(double) ?
重载函数与转换构造函数
struct C {
C(int);
};
struct D {
D(int);
};
void manip(const C&);
void manip(const D&);
// manip(10);
重载函数与用户定义的类型转换
struct E {
E(double);
};
void manip2(const C&);
void manip2(const E&);
// manip(10);
14.9.3 函数匹配与重载运算符
重载运算符也是重载的函数,也适用于普通函数的匹配规则。
而且也要注意成员函数和非成员函数彼此不会重载。
class SmallInt {
friend SamllInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int = 0);
operator int() const { return val; }
private:
std::size_t val;
};
SmallInt s1, s2;
SmallInt s3 = s1 + s2;
int i = s3 + 0; // 二义性错误!
Next: 《C++ Primer》 拾遗 第 16 章 模板与泛型编程