第十四章 重载运算与类型转换
基本概念
以源运算符有一个参数,二元运算符有两个。对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。除了重载的函数调用运算符 operator()
之外。其他重载函数不能还有默认实参。
对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数:
// 错误:不能为 int 重定义内置的运算符
int operator+(int, int);
这一约定意味着当运算符作用于内置运算类型的运算对象时,我们无法改变该运算符的含义。
可以被重载的运算符 | |||||
---|---|---|---|---|---|
+ | - | ** | / | % | ^ |
& | | | ~ | ! | , | = |
< | > | <= | >= | ++ | – |
<< | >> | == | != | && | || |
+= | -= | /= | %= | ^= | &= |
|= | *= | <<= | >>= | [] | () |
-> | ->* | new | new[] | delete | delete[] |
不能被重载的运算符 | |||||
:: | .* | . | ? : |
直接调用一个重载的运算符函数
通常情况下,我们将运算符作用于类型这边过去也的实参,从而以这种间接方式“调用”重载的运算符函数。然而,也能像调用普通函数一样直接调用运算符函数,先指定函数名字,然后传入数量正确、类型适当的实参:
// 一个非成员运算符函数的等价调用
data1 + data2; // 普通的表达式
operator+(data1, data2); // 等价的函数调用
这两次调用时等价的,它们都调用了非成员函数 operator+
, 传入 data1
作为第一个实参,传入 data2
作为第二个实参。
我们像调用其他成员函数一样显示地调用成员运算符函数。具体做法是,首先指定运行函数地对象(或指针)地名字,然后用点运算符(或箭头运算符)访问希望调用地函数:
data1 += data2; // 基于 “调用” 地表达式
data1.operator+=(data2); // 对成员运算符函数的等价调用
选择作为成员或者非成员
当我们定义重载的运算符时,必须首先决定将其声明为类的成员函数还是声明为一个普通的非成员函数。
- 赋值(
=
)、下标([ ]
)、调用(( )
) 和成员访问箭头(->
) 运算符必须是成员 - 复合赋值运算符一般来说应该是成员,但并非必须,这一点于赋值运算符略有不同。
- 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员
- 具有对称性的运算符可能转换任意一端的运算对象,例如算数、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数
当我们把运算符定义成成员函数时,它的左侧运算对象必须时运算符所属类的一个对象:
string s = "world";
string t = s + "!"; // 正确: 我们能把一个 const char* 加到一个 string 对象中
string u = "hi" + s; // 如果 + 是 string 的成员,则产生错误
如果 operator+
是 string
类的成员,则上面的第一个加法等价于 s.operator("!")
。同样的 "hi" + s
等价于 "hi".operator+(s)
。显然 "hi"
的类型是 const char*
,这是一种内置类型,根本没有成员函数
因为 string
将 +
定义成了普通的非成员函数,所以 "hi" + s
等价于 operator+("hi", s)
。和任何其他函数调用一样,每个实参都能被转换成形参类型。唯一的要求是子很少有一个运算对象是类类型,并且两个运算对象都能准确无误地转换成 string
。
输入和输出运算符
重载输出运算符 <<
通常情况下,输出运算符的第一个形参是一个非常量 ostream
的引用。
第二个形参一般来说是一个常量引用,该常量是我们想要打印的类类型。
ostream& operator<<(ostream& os, const Sales_data& item){
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
输出运算符尽量减少格式化操作
用于内置类型的输出运算符不太考虑格式化操作,尤其不会打印换行符,用户希望类的输出运算符也像如此行事。如果运算符打印了换行符,则用户就无法在对象的同一行内接着打印一些描述性的文本了。相反,令输出运算符尽量减少格式化操作可以使用户有权控制输出的细节。
通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符
输入输出运算符是非成员函数
与 iostream
标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数。否则,它们的左侧运算对象将是我们的类的一个对象
Sales_data data;
data << cout; // 如果 operator<< 是 Sales_data 的成员
以前的笔记:
如果我们希望为类自定义 IO
运算符,则必须将其定义成非成员函数。当然,IO
运算符通常需要读写类的非公有数据成员,所以 IO
运算符一般被声明为友元
重载输入运算符 >>
Sales_data 的输入运算符
istream& operator>>(istream& is, Sales_data& item){
double price; // 不需要初始化,因为我们将先读入数据到 price, 之后才使用它
is >> item.bookNo >> item.units_sold >> price;
if( is ) // 检查输入是否成功
item.revenue = item.units_sold * price;
else
item = Sales_data(); // 输入失败:对象被赋予默认的状态
return is;
}
输入运算符必须处理输入可能失败的情况,而输出运算符不需要
输入时的错误
在执行输入运算符时可能发生下列错误
- 当流含有错误类型的数据时读取操作可能失败。例如在读取完
bookNo
后,输入运算符假定接下来读入的是两个数字数据,一旦输入的不是数字数据,则读取操作及后续对流的其他使用都将失败 - 当读取操作到达文件末尾或者遇到输入流的其他错误时也会失败
当读取操作发生错误时,输入运算符应该负责从错误中恢复
算术和关系运算符
如果类定义了算术运算符,则它一般也会顶一个一个对应的符合赋值运算符。此时最有效的方式是使用符合赋值来定义算数运算符
Sales_data
operator+(const Sales_data& lhs, const Sales_data& rhs){
Sales_data sum = lhs;
sum += rhs;
return sum;
}
相等运算符
bool operator==(const Sales_data& lhs, const Sales_data& rhs){
return lhs.isbn() == rhs.isbn() &&
lsh.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data& lhs, const Sales_data& rhs){
return !(rhs == lhs);
}
这些函数体现了设计准则:
- 如果一个类含有判断两个对象是否相等的操作,则它显然应该把函数定义成
operator==
而非一个普通的命名函数: 因为用户肯定希望能使用==
比较对象,所以提供了==
就意味着用户无须再费时费力地学习并记忆一个全新的函数名字,此外,类定义了==
运算符之后也更容易使用标准库容器和算法 - 如果类定义了
operator==
,则该运算符应该能判断一组给定对象中是否含有重复数据 - 通常情况下,相等运算符应该具有传递性,换句话说,如果
a == b
和b == c
都为真,则a == c
也应该为真 - 如果类定义了
operator==
, 则这个类也应该定义operator!=
。 - 相等运算符和不相等运算符中的一个应该把工作委托给另一个,这意味着其中一个运算符应该负责实际比较对象的工作,而另一个运算符只是调用那个真正工作的运算符
关系运算符
如果存在唯一一种逻辑可靠的
<
定义,则应该考虑为这个类去定义<
运算符。如果类同时还包含==
,则当且仅当<
的定义和==
产生的结果一致时菜定义<
运算符。比如,对于Sales_data
类,因为可比较的类成员多,所以不适合做<
赋值运算符
在拷贝赋值和移动赋值运算符之外,标准库 vector
类还定义了第三种赋值运算符,该运算符接受花括号内的元素列表作为参数
vector<string> v;
v = {"a", "an", "the"};
同样,也可以把这个运算符添加到 StrVec
类中
class StrVec{
public:
StrVec& operator=(std::initializer_list<std::string>);
// ...
};
StrVec& StrVec::operator=(std::initializer_list<std::string> il){
// alloc_n_copy 分配内存空间并从给定范围内拷贝元素
auto data = alloc_n_copy(il.begin(), il.end());
free(); // 销毁对象中的元素并释放空间
elements = data.first; // 更新苏韩剧成员时期指向新空间
first_free = cap = data.second;
return *this;
}
复合赋值运算符
// 作为成员的二元运算符: 左侧运算对象绑定到隐式的 this 指针
// 假定两个对象表示的是同一本书
Sales_data& Sales_data::operator+=(const Sales_data& rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这样做。这两类运算符都应该返回左侧运算对象的引用
下标运算符
class StrVec{
public:
std::string& operator[](std::size_t n)
{ return elements[n]; }
const std::string& operator[](std::size_t n) const
{ return elements[n]; }
// ...
private:
std::string* elements; // 指向数组首元素的指针
}:
// 假设 svec 是一个 StrVec 对象
const StrVec cvec = svec; // 把 svec 的元素拷贝到 cvec 中
// 如果 svec 中含有元素,对第一个元素运行 string 的 empty 函数
if( svec.size() && svec[0].empty() ){
svec[0] = "zero"; // 正确:下标运算符返回 string 的引用
cvec[0] = "zip"; // 错误: 对 cvec 取下标返回的是常量引用
}
递增和递减运算符
定义前置递增/递减运算符
class StrBlobPtr{
public:
// 递增和递减运算符
StrBlobPtr& operator++();
StrBlobPtr& operator--();
// ...
};
递增和递减运算符的工作机制非常相似: 首先调用 check
函数检验 StrBlobPtr
是否有效,如果是,接着检查给定的索引值是否有效。如果 check
函数没有抛出异常,则运算符返回对象的引用。
在递增运算符的例子中,我们把 curr
的当前值传递给 check
函数。如果这个值小于 vector
的大小,则 check
正常返回;否则,如果 curr
已经到达了 vector
的末尾,check
抛出异常
StrBlobPtr& operator++(){
// 如果 curr 已经指向了容器的尾后位置,则无法递增它
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
StrBlobPtr& operator--(){
// 如果 curr 已经指向了容器的尾后位置,则无法递增它
check(curr, "decrement past begin of StrBlobPtr");
--curr;
return *this;
}
区分前置和后置运算符
要想同时定义前置和后置运算符,必须首先解决普通的重载形式无法区分这两种情况的问题。
为了解决这个额问题,后置版本接受一个额外的(不被使用) int
类型的形参。当我们使用后置运算符时,编译器为这个形参提供一个值为 0
的实参。尽管从语法上来说后置运算符可以使用这个额外的形参,但是在实际过程中通常不会这么做。这个而形参的唯一作用就是区分前置和后置版本你的函数,而不是真的要在实现后置版本时参与运算
class StrBlobPtr{
public:
// 递增和递减运算符
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
// ...
};
为了和内置版本保持一致,后置运算符应该返回对象的原值( 递增和递减之前的值 ),返回的形式是一个值而非引用
对于后置版本来说,在递增对象之前需要首先记录对象的状态
StrBlobPtr operator++(int){
// 此处无须家产有效性,调用前置递增运算符时才需要检查
StrBlobPtr ret = *this; // 记录当前值
++*this; // 向前移动一个元素,前置 ++ 需要检查
// 递增的有效性
return ret; // 返回之前记录的状态
}
StrBlobPtr operator--(int){
// 此处无须家产有效性,调用前置递减运算符时才需要检查
StrBlobPtr ret = *this; // 记录当前值
--*this; // 向后移动一个元素,前置 -- 需要检查
// 递增的有效性
return ret; // 返回之前记录的状态
}
由上可知,后置运算符调用各自的前置版本来完成实际的工作。
因为我们不会用到
int
形参,所以无须为其命名
显示地调用后置运算符
StrBlobPtr p(a1); // p 指向 a1 中的 vector
p.operator++(0); // 调用后置版本的 operator++
p.operator++(); // 调用前置版本的 operator++
尽管传入的值通常会被运算符函数忽略,但却必不可少,因为编译器只有通过它才能知道应该使用后置版本
成员访问运算符
class StrBlobPtr{
public:
std::string& operator*() const{
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) 是对象所指的 vector
}
std::string* operator->() const{
// 将实际工作委托给解引用运算符
return & this->operator*();
}
// ...
};
解引用运算符首先检查 curr
是否仍在作用范围内,如果是,则放回 curr
所指元素的一个引用。箭头运算符不执行任何自己的操作,而是调用解引用运算符并返回解引用结果元素的地址
箭头运算符必须是类的成员。解引用运算符通常也是类的成员。
函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通函数相比它们更灵活
struct absInt{
int operator()(int val) const{
return val < 0 ? -val : val;
}
};
这个类值定义了一种操作: 函数调用运算符,它负责接收一个 int
类型的实参,然后取回该实参的绝对值
我们使用调用运算符的方式是另一个 absInt
对象作用于一个实参列表,这一过程看起来非常像调用函数的过程
int i = -42;
absInt absObj; // 含有函数调用运算符的对象
int ui = absObj(i); // 将 i 传递给 absObj.operator()
即使 absObj
只是一个对象而非函数,我们也能 “调用” 该对象。调用对象实际上是在运行重载的调用运算符。在此例中,该运算符接收一个 int
值并返回其绝对值
函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,但参数上应该有所区别
如果类定义了调用运算符,则该类的对象称作函数对象(function object)。函数对象通常含有一些数据成员,这些成员被用于定制调用运算符中的操作。
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; // 用于将不同输出隔开的字符
};
类有一个构造函数,接收一个输出流的引用以及一个用于分隔的字符,这两个形参的默认实参分别是 cout
和空格。之后的函数调用运算符使用这些成员协助其打印给定的 string
PrintString printer; // 使用默认值,打印到 cout
printer(s); // 在 cout 中打印 s,后面跟一个空格
PrintString errors(cerr, '\n');
errors(s); // 在 cerr 中打印 s, 后面跟一个换行符
函数对象常常作为泛型算法的实参。例如,可以使用标准库 for_each
算法和我们自己的 PritntString
类来打印容器的内容
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
for_each
的第三个实参是类型 PrintString
的一个临时对象,其中我们用 cerr
和 换行符初始化了该对象。当程序调用 for_each
时,将会把 vs
中的每个元素一次打印到 cerr
中,元素之间以换行符分隔
lambda 是函数对象
PrintString
对象作为调用 for_each
的实参,这一用法类似于使用 lambda
表达式的程序。当我们编写了一个 lambda
后,编译器将该表达式作为一个未命名类的未命名对象。在 lambda
表达似乎产生的类中含有一个重载的函数调用运算符,例如,对于我们传递给 stable_sort
作为其最后一个实参的 lambda
表示来说:
// 根据单词的长度对其进行排序,对于长度相同的单词按照字母表顺序排序
stable_sort(words.begin(), words.end(),
[](const string* a, const string& b)
{ return a.size() < b.size(); });
其行为类似于下面这个类的一个未命名对象
class ShorterString{
public:
bool operator()(const string& s1, const string& s2) const
{ return s1.size() < s2.szie(); }
};
产生的类只有一个函数调用运算符成员,它负责接收两个 string
并比较它们的长度,它的形参列表和函数体与 lambda
表达式完全一样。
用这个类替代 lambda
表达式后,可以重写并重新调用 stable_sort
stable_sort(words.begin(), words.end(), ShorterString());
// 之前的几种写法
// 比较函数,用来按长度排序单词
bool isShorter(const string& s1, const string& s2){
return s1.size() < s2.size();
}
// 按长度由短到长排序 words
sort(words.begin(), words.end(), isShorter);
第三个实参是新构建的 ShorterString
对象,当 stable_sort
内部的代码每次比较两个 string
时就会调用这一对象,此时该对象将调用运算符的函数体,判断第一个 string
的大小小于第二个时返回 true
表示 lambda 及相应捕获行为的类
当一个 lambda
表达式通过引用捕获变量时,将由程序负责确保 lambda
执行时所引的对象确实存在。因此,编译器可以直接使用该引用而无须在 lambda
产生的类中将其存储为数据成员
相反,通过值捕获的变量被拷贝到 lambda
中。因此,这种方式产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员
// 获得第一个指向满足条件元素的迭代器,该元素满足 size() is >= sz
auto wc = find_if(word.begin(), word.end(), [sz](const string& a)
{ return a.szie() >= sz; })
该 lambda
表达式产生的类将形如:
class SizeComp{
public:
SizeComp(size_t n):sz(n){} // 该形参对应捕获的变量
// 该调用运算符的返回类型、形参和函数体都与 lambda 一致
bool operator()(const string& s) const
{ return s.size() >= sz; }
private:
size_t sz;
};
和 ShorterString
类不同,上面这个类含有一个数据成员以及一个用于初始化该成员的构造函数。这个合成的类不含有默认构造函数,因此想要使用这个类必须提供一个实参。
auto wc = find_if(word.begin(), word.end(), SizeComp(sz));
lambda
表达式产生的类不含默认构造函数、赋值运算符以及默认析构函数: 它是否含有默认的拷贝/移动构造函数通常要视捕获的数据成员类型而定
标准库定义的函数对象
plus<int> intAdd; // 可执行 int 加法的函数对
negate<int> intNegate; // 可对 int 值求反的函数对象
// 使用 intAdd::operator(int, int) 求 10 和 20 的和
int sum = intAdd(10, 20); // 等价于 sum = 30
sum = intNegate(intAdd(10, 20)); // 等价于 sum = -30
// 使用 intNegate::operator(int) 生成 -10
// 然后将 -10 作为 intAdd::operator(int, int) 的第二个参数
sum = intAdd(10, intNegate(10)); // sum = 0
下表类型定义在 functional
头文件中
算术 | 关系 | 逻辑 |
---|---|---|
plus<Type> | equal_to<Type> | logical_and<Type> |
minus<Type> | not_equal_to<Type> | logical_or<Type> |
multiplies<Type> | greater<Type> | logicalnot<Type> |
divides<Type> | greater_equal<Type> | |
modulus<Type> | less<Type> | |
negate<Type> | less_equal<Type> |
在算法中使用标准库函数对象
表示运算符的函数对象类常用来替换算法中的默认运算符。比如,在默认情况下使用排序算法使用 operator<将序列按照升序排序>
。如果要执行降序排列的话,我们可以传入一个 greater
类型的对象。该类将产生一个调用运算符并负责执行待排序类型的大于运算。例如,如果 svec
是一个 vector<string>
// 传入一个临时的函数对象用于执行两个 string 对象的 > 比较运算
sort(svec.begin(), svec.end(), greater<string>());
需要特别注意的时,标准库规定其函数对象对于指针同样适用。之前介绍比较两个无关指针将产生未定义的行为,然而可能会希望通过比较指针的内存地址来 sort
指针的 vector
。直接这么做将产生未定义的行为,因此可以使用一个标准库函数对象来实现该目的
vector<string*> nameTable;
// 错误: nameTable 中的指针彼此之间没有关系,所以 < 将产生未定义的行为
sort(nameTable.begin(), nameTable.end(),
[](string* a, string* b){ return a < b; })
// 正确: 便准库规定的指针的 less 是定义良好的
sort(nameTable.begin(), nameTable.end(), less<string*>());
关联容器使用 less<ket_type>
对元素排序,因此我们可以定义一个指针的 set
或者在 map
中使用指针作为关键值而无须直接声明 less
可调用对象与 function
和其他对象一样,可调用的对象也有类型。例如,每个 lambda
有它自己唯一的( 未命名 )类类型;函数及函数指针的类型则由其返回值类型和实参类型决定,等等。
然而,两个不同类型的可调用对象却可能共享同一种调用形式( call signature)。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型,例如:
int(int, int)
是一个函数类型,它接受两个 int
、返回一个 int
不同类型可能具有相同的调用形式
对于几个可调用对象共享同一种调用形式的情况,有时我们会希望把它们看成具有相同的类型。例如:
// 普通函数
int add(int i, int j) { return i + j; }
// lambda, 其产生一个未命名的对象类
auto mod = [](int i, int j){ return i % j; }
// 函数对象类
struct divide{
int operator()(int denominator, int divisor){
return denominator / divisor;
}
};
上诉可调用对象共享同一种调用形式
int(int, int)
我们可能希望使用这些可调用对象构建一个简单的桌面计算器。为了实现这一目的,需要定义一个函数表( function table ) 用于存储指向这些可调用对象的“指针”。当程序需要执行某个特定的操作时,从表中查找该调用的函数。
// 构建从运算符到函数指针的映射关系,其中函数接收两个 int、返回一个 int
map<string, int(*)(int, int)> binops;
我们可以按照下面的形式将 add
指针添加到 binops
中
// 正确: add 是一个指向正确类型函数的指针
binops.insert({"+", add}); // {"+", add} 是一个 pair
但是我们不能将 mod
或者 divide
存储 binops
binops.insert({"%", mod}); // 错误: mod 不是一个函数指针
问题在于 mod
是个 lambda
表达式,而每个 lambda
有它自己的类类型,该类型与存储在 binops
中的值的类型不匹配
标准库 function 类型
可以使用一个名为 function
的新标准库类型解决上述问题。
操作 | 解释 |
---|---|
function<T> f | f 是一个用来存储可调用对象的空 function ,这些可调用对象的调用形式应该与函数类型 T 相同 |
function<T> f(nullptr) | 显式地构造一个空 function |
function<T> f(obj) | 在 f 中存储可调用对象 obj 的副本 |
f | 将 f 作为条件:当 f 含有一个可调用对象时为真,否则假 |
f(args) | 调用 f 中的对象,参数 args |
定义为 function 的成员的类型 | |
result_type | 该 function 类型的可调用对象返回的类型 |
argument_type first_argument_type second_argument_type | 当 T 有一个或两个实参时定义的类型。如果 T 只有一个实参,则 argument_type 是该类型的同义词; 如果 T 有两个实参,则 first_argument_type 和 second_argument_type 分别代表两个实参的类型 |
function<int(int, int)>
在此,我们声明了一个 function
类型,它可以表示接收两个 int
、返回一个 int
的可调用对象。因此,我们可以用这个新声明的类型表示任意一种桌面计算器用到的类型
function<int(int, int)> f1 = add; // 函数指针
function<int(int, int)> f2 = divide(); // 函数对象类的对象
function<int(int, int)> f3 = [](int i, int j) // lambda
{ return i * j; };
cout << f1(4, 2) << endl; // 6
cout << f2(4, 2) << endl; // 2
cout << f3(4, 2) << endl; // 8
使用这个 function
类型我们可以重新定义 map
// 列举了可调用对象与二元运算符对应关系的表格
// 所有可调用对象都必须接收两个 int、返回一个 int
// 其中的元素可以是函数指针、函数对象或者 lambda
map<string, function<int(int, int)>> binops;
map<string, function<int(int, int)>> binops = {
{"+", add}, // 函数指针
{"-", std::minus<int>()}, // 标准库函数对象
{"/", divide()}, // 用户定义的函数对象
{"*", [](int i, int j){ return i * j; }} // 未命名的 lambda
{"%", mod} // 命名了的 lambda 对象
};
binops["+"](10, 5);
binops["-"](10, 5);
binops["/"](10, 5);
binops["*"](10, 5);
binops["%"](10, 5);
重载的函数与 function
我们不能(直接)将重载函数的名字存入 function
类型的对象中
int add(int i, int j){ return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert({"+", add}); // 错误
解决上述二义性问题的一条途径是存储函数指针而非函数的名字:
int(*fp)(int, int) = add; // 指针所指的 add 是接收两个 int 的版本
binops.insert({"+", fp}); // 正确:fp 指向一个正确的 add 版本
同样,使用 lambda
也可以消除二义性
// 正确:使用 lambda 来指定我们希望使用的 add 版本
binops.insert({"+", [](int a, int b){ return add(a, b);}});
lambda
内部的函数调用传入了两个 int
, 因此该调用只能匹配接收两个 int
的 add
版本,而这也正是执行 lambda
时真正调用的函数。
重载、类型转换与运算符
转换构造函数和类型转换运算符共同定义了类类型转换运算符(class-type conversions), 这样的转换有时也被称作用户定义的类型转换(user-defined conversions)
类型转换运算符
类型转换运算符(conversion operator) 是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下所示:
operator type() const;
其中 type
表示某种类型。类型转换运算符可以面向任意类型(除了 void
以外)进行定义,只要该类型能作为函数的返回类型。因此,不允许转换成数组或者函数类型,但允许转换成指针或者引用类型。
一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表页必须为空。类型转换函数通常应该是
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;
};
SmallInt
类及定义了向类类型的转换,页定义了从类类型向其他类型的转换。其中,构造函数将算数类型的值转换成 SmallInt
对象,而类型转换运算符将 SmallInt
对象转换成 int
SmallInt si;
si = 4; // 首先将 4 隐式地转换成 SmallInt, 然后调用 SmallInt::operator=
si + 3; // 首先将 si 隐式地转换成 int,然后执行整数的加法
class SmallInt;
operator int(SmallInt&); // 错误:不是成员函数
class SmallInt{
public:
int operator int() const; // 错误:指定了返回类型
operator int(int = 0) const; // 错误:参数列表不为空
operator int*() const { return 42; } // 错误:42 不是一个指针
};
显式的类型转换运算符
class SmallInt{
public:
// 编译器不会自动执行这一类型转换
explcit operator int() const { return val; }
// ...
}:
SmallInt si = 3; // 正确:SmallInt 的构造函数不是显式的
si + 3; // 错误:此处需要隐式的类型转换,但类的运算符是显式的
static_cast<int>(si) + 3; // 正确:显式地请求类型转换