当运算符作用于类类型的运算对象时,可以通过运算符重载重新定义运算符的含义。
14.1 基本概念
重载的运算符时具体特使名字的函数:由关键字operator和运算符号组成。和其它函数一样,重载的运算符也包括返回类型、参数列表和函数体。
重载运算符函数和参数数量和该运算符作用的运算对象数量一样多。一元一个,二元两个。除了重载的函数调用运算符operator()之外,其它重载运算符不能有默认实参。
如果一个运算符函数时成员函数,则第一个(左侧)运算对象绑定到隐式的this指针上。因此成员运算符函数和(显式)参数数量比运算符的运算对象总数少一个。
运算符函数必须时类的成员,或至少含有一个类类型的参数。无法作用于内置类型
int operator+(int , int) //错误,不能为int重定义内置的运算符
我们只能重载不能发明符号,所有的符号见表14.1, (+ - * &)即使一元又是二元。运算符的优先级和结合律与对应的内置一样
直接调用一个重载和运算符函数
类似普通函数
data1 + data2;
operator+(data1, data2); //等价函数调用
成员运算符函数也可以用点或箭头来调用
data1 += data2;
data1.operator+=(data2);
某些运算符不应该被重载
某些运算符指定了运算对象求值的顺序,这些规则无法应用到重载的运算符。特别时逻辑与或和逗号运算符的对象求值顺序规则无法保留。此外&&和||运算符的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。
还有一个原因:C++语言定义了两种运算符用于类类型对象时的特殊含义,一般来说不应该重载,否则行为异于常态用户无法适应
使用与内置类型一致的含义
如果某些操作在逻辑上与运算符相关,则它们适合于定义成重载的运算符:
- 如果类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致
- 如果类的某个操作检查相等性,则定义operator==;同时也应有operator!=
- 如果类包含一个内在的单序比较操作,则定义operator<;如果有了一个,同时也应该含有其它关系操作
- 重载运算符的返回类型通常应该和其内置版本的返回类型兼容:逻辑和关系返回bool,算术返回一个类类型,赋值和复合赋值运算符则应该返回左侧运算对象的一个引用
赋值和复合赋值运算符
赋值运算符的行为和复合版本类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用。重载的赋值运算应该继承而非违背其内置版本的含义
如果类含有算术运算符或者位运算符,则最好也提供对应的复合赋值运算符。
选择作为成员或者非成员
准则:
- 赋值=、下标[]、调用()、和成员访问运算符必须是成员
- 复合赋值运算符一般来说应该是成员,但并非必须
- 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增递减解引用,通常是成员
- 具有对称性的运算符可能转换任意一端的运算对象,例如算数、相等、关系、位运算等,因此通常为非成员函数
如果想提供含有类对象的混合类型表达式,则必须定义为非成员函数
当我们把运算符定义成成员函数时,它左侧运算对象必须时运算符所属的一个对象,例:
string s = "world";
string t = s + "!"; //正确
string u = "hi" + s; //如果+是成员函数会错误
如果是成员函数则等价s.operator+("!"),而const char*没有成员函数。如果是非成员函数,等价为operator("hi", s)就可以换位置。
14.2 输入和输出运算符
和<<,类需要自定义适合其对象的新版本以支持IO操作
14.2.1 重载输出运算符<<
通常情况下,<<的第一个形参是一个非常量ostream对象的引用,因为向流内写入会改变其状态,且无法复制
第二个形参是常量的引用,为打印的类类型。
为了与其它输出保持一致,operator<<一般要返回它的ostream形参
Sales_data的输出运算符
ostream &operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
输出运算符尽量减少格式化操作
如果运算符打印了换行符,则用户就无法在对象的同一行内接着打印其它描述性文本。尽量减少格式化操作可以使用户有权控制输出的细节
输入输出运算符必须是非成员函数
与iostream兼容的必须是成员函数,否则它的左侧运算对象是我们类的一个对象
IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元
14.2.2 重载输入运算符>>
通常第一个形参是要读取的流的引用,第二个形参是要读入到非常量对象的引用。
Sales_data的输入运算符
istream &operator>>(istream &is, Sales_data &item)
{
double price;
is >> item.bookNo >> item.units_sold >> price;
if (is)
item.revenue = item.units_sold * price;
else
item = Sales_data();
return is;
}
输入运算符必须处理输入可能失败的情况,而输出运算符不需要
输入时的错误
可能发生如下错误:
- 当流含有错误类型的数据时读取操作可能失败
- 当读取操作达到文件末尾或者遇到输入流的其它错误时也会失败
在上个例子中没有逐个检查,而是读取完后再一次性检查。如果失败则重置为空Sales_data
当读取操作发生错误时,输入运算符应该负责从错误中恢复
标示错误
一些输入运算符需要做更多数据验证的工作。例如例子中的需要检查bookNo是否复合规范的格式。即使从技术上来看IO时成功的,输入运算符也应该设置流的条件状态以表示出失败信息。通常情况下输入运算符只设置failbit,设置eofbit表示文件耗尽,badbit表示流被破坏。最好的方式是由IO标准库自己来表示这些错误
14.3 算术和关系运算符
通常定义为非成员函数以允许对左侧或右侧对象进行转换。且形参通常为常量的引用,不需要改变运算对象。
算术运算符操作完成后返回该局部变量的副本作为结果,一般也定义一个复合复制运算符
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum += rhs;
return sum;
}
通常情况下用复合赋值来实现算术运算
14.3.1 相等运算符
一般用来检验两个对象是否相等
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() &&
lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs);
}
设计准测:
- 如果一个类含有判断两个对象是否相等的操作,用operator==而非普通的函数命名
- 如果类定义了operator==,则该运算符应该能判断一组给定的对象中是否含有重复数据
- 通常情况下,相等运算符应该由传递性
- 如果也operator==,也应该定义operator!=.反之亦然
- 相等运算和不等运算中的其中一个应该把工作委托给另一个
14.3.2 关系运算符
定义了相等运算符的类常常包含关系运算符,特别的,因为关联容器和一些算法要用到小于运算符,所以定义operator<会比较有用
通常下关系运算符应该:
- 定义顺序关系,令其与关联容器中对关键字的要求一致
- 如果类同时含有,则定义一个关系令其与保持一致。特别是,如果两个对象是!=的,那么一个对象应该<另一个
对于Slaes_data来说,光比较revenue和units_sold而无视ISBN,没有意义。不存在一种逻辑可靠的<定义,这个类不定义<就更好
14.4赋值运算符
除了拷贝赋值和移动赋值运算符,类还可以定义其它赋值运算符以使用别的类型作为右侧运算对象
例:vector定义了第三种赋值运算符,接受花括号内的元素列表作为参数
vector<string> v;
v = {"a", "an", "the"};
同样也可以把这个类加入StrVec中:
class StrVec{
public:
StrVec &operator=(std::initializer_list<std::string>);
};
StrVec &StrVec::operator=(initializer_list<std::string> il)
{
auto data = alloc_n_copy(il.begin(), il.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
和拷贝赋值和移动赋值一样,其它重载的赋值运算符也必须先释放当前内存空间再创建一片新空间,不过无须检查对象向自身的赋值,因为形参不同
复合赋值运算符
倾向与把包括复合赋值在内的所有赋值运算定义在类的内部,为了和内置类型的复合赋值保持一致,类中的复合赋值运算符也要返回其左侧对象的引用
Sales_data &Sales_data::operator+=(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
14.5 下标运算符
表示容器的类通常可以访问元素在容器中的位置访问元素,这些类一般会定义下标运算符operator[],必须是成员函数
通常返回所访问元素的引用,且最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会给返回的对象赋值
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; //指向数组首元素的指针
}
当StrVec是非常量时,可以赋值;对常量对象取下标时不能赋值
const StrVec cvec = svec;
if (svec.size() && svec[0].empty()){
svec[0] = "zero"; //正确
cvec[0] = "Zip"; //错误,这是常量引用
}
14.6 递增和递减运算符
++和--,使得类可以在元素的序列中前后移动,通常设定为成员函数.
对于内置类型来说,既有前置版本又有后置版本,同样为类定义也应该这样
定义前置递增递减运算符
class StrBlobStr{
public:
StrBlobStr& operator++(); //前置运算符
StrBlobStr& operator--();
}
工作机理:先调用check,再检查给定的索引值是否有效,如果无异常则返回对象的引用
StrBlobStr& StrBlobStr:: operator++()
{
//如果curr已经指向了容器尾后位置,无法递增
check(curr, "increment past end of StrBlobStr");
++curr;
return *this;
}
StrBlobStr& StrBlobStr::operator--()
{
//如果curr是0,继续递减会产生无须下标
--curr;
check(curr, "decrement past begin of StrBlobStr");
return *this;
}
区分前置和后置运算符
普通的重载无法区分,所以后置版本接受一个额外的(不被使用的)int类型的形参。当使用后置运算符是,编译器为这个形参提供一个值为0的实参。通常不使用这个形参
class StrBlobStr{
public:
StrBlobStr operator++(int);
StrBlobStr operator--(int);
}
后置版本在递增前需要首先记录对象的状态
StrBlobStr StrBlobStr::operator++(int)
{
//此处无须检查,调用前置运算符才需要检查
StrBlobStr ret = *this; //记录当前值
++*this;
return ret;
}
StrBlobStr StrBlobStr::operator--(int)
{
StrBlobStr ret = *this; //记录当前值
--*this;
return ret;
}
因为用不到int实参,所以不需要命名
显示地调用后置运算符
StrBlobPtr p(al);
p.operator++(0); //后置版本
p.operator--(); //前置版本
14.7 成员访问运算符
在迭代器类及智能指针类中会用到*和->运算符,向StrBlobPtr条件这两种运算符
class StrBlobPtr{
public:
std::string& operator*() const;
{
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
std::string* operator->() const
{
return & this->operator*();
}
}
解引用首先检查curr是否在作用范围内,如果是则返回curr所指元素的一个引用
箭头运算符不执行任何自己的操作,而是调用解引用运算符并返回解引用结果元素
箭头运算符必须是类成员,解引用通常也是类成员
这两个运算符定义成const,因为不会改变其状态。
StrBlob al = {"hi", "bye"};
StrBlobPtr p(a1);
*p = "okey";
cout << p->size() << endl; //打印4
cout << (*p).size() << endl; //等价写法
对箭头运算符返回值的限定
箭头运算符应该获取成员,重载时可以该表的是箭头从哪个对象中获取成员
例:point->mem,point必须是指向类对象的指针或者是一个重载了operator->类的对象。根据point的不同,point->mem分别等价于
(*point).mem; //是指针
point.operator()->mem; //是类的对象
除此之外外码都会发生错误。point->mem的执行过程如下:
- 如果point是指针,则用内置的箭头运算符.如果point所指类没有mem成员则错误
- 如果point定义了operator->的类的一个对象,则使用point.operator()->()的结果来获取mem。如果结果是一个指针则执行1,否则重复调用2.最终当这一过程结束时程序或者返回了所需的内容,或者返回一些表示错误的信息。
14.8 函数调用运算符
如果类重载了函数调用运算符,则也可以像函数一样使用该类的对象。因为这样的类同时也能存储状态,所以比普通函数更灵活
例:absInt的struct含有一个调用运算符,返回其参数的绝对值
struct absInt{
int operator()(int val) const{
return val < 0 : -val :val;
}
}
这个类只定义了函数调用运算符,负责接受一个int类型实参然后返回绝对值
int i = -42;
absInt absObj;
int ui = absObj(i); //看起来很像函数调用
absObj是一个对象,调用对象实际上是运行重载的调用运算符.
函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有区别。
如果类定义了调用运算符,则该类的对象称作函数对象。
含有状态的函数对象类
函数对象类通常含有一些数据成员,被用于定制调用运算符中的操作。
例:定义一个打印string实参内容的类。默认每个string直接空格隔开,也允许提供其它可写流或分隔符
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;
}
当定义PrintString对象时,对于分隔符及输出流既可以使用默认值也可以提供我们自己的值:
PrintString printer;
printer(s); //在cout中打印s,后面跟一个空格
PrintString errors(cerr, '\n');
errors(s); //则cerr中打印s,后面跟一个换行
函数对象常常作为泛型算法的实参,例如可以用for_each和PrintString类来打印容器的内容
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
14.8.1 lambda是函数对象
在lambda表达式产生的类中含有一个重载的函数调用运算符。例:
stable_sort(words.begin, words.end(),
[](const string &a, const string &b)
{return a.size(0 < b.szie();));
其行为类似下面这个类的一个未命名的对象
class ShorterString{
public:
bool operator()(const string &s1, const string &s2) const
{ return s1.size() < s2.size(); }
};
默认情况下lambda产生的类当中的函数调用运算符是一个const成员函数。如果lambda被声明为可变的则调用运算符就不是const的
用这个类代替lambda
stable_sort(words.begin(), words.end(), ShorterString());
第三个实参是构建新的ShorterString对象,当stable_sort每次比较时就调用这个对象
表示lambda以及相应捕获行为的类
通过引用捕获的变量无须存储为数据成员,用值捕获的变量会被拷贝到lambda中,就会产生对应的数据成员,同时创建构造函数
auto wc = find_if(words.begin(), word.end(),
[sz](const string &a)
{ reutnr a.size() >= sz; });
//该lambda表达式产生的类会是:
class SizeComp{
SizeComp(size_t n): sz(n) {}
bool operator()(const string &s) const
{return s.szie() >= sz; }
private:
size_t sz;
}
这个合成的类不含有默认构造函数,因此想使用必须提供一个实参
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));
lambda表达式产生的类不含有默认构造函数、赋值运算和默认析构。默认的移动构造和拷贝构造函数通常根据捕获的数据成员类型而定
14.8.2 标准库定义的函数对象
标注库自定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。例:plus类定义了一个函数调用运算符用于对一个运算对象执行+的操作;modulus类定义了一个调用运算符执行二元的%操作
这些类都定义成模板的形式,我们可以指定具体的应用类型。例:plus
plus<int> intAdd; //可执行int加法的函数
negate<int> intNegate; //可对int取反的函数对象
//使用intAdd:operator(int,int)求10和20的和
int sum = intAdd(10,20);
sum = inNegate(intAdd(10, 20));
sum = intAdd(10, intNegate(10)); //sum = 0
更多见表14.2,都定义在functional头文件中
在算法中使用标准库的函数对象
表示运算符的函数对象常常用来替换算法中的默认运算符。比如排序是用<来升序排列的。如果要降序则可以传入一个greater类型的对象,产生一个调用运算符并负责执行待排序类型的大于运算
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; });
//正确
sort(nameTable.begin(), nameTable.end(), less<string*>());
关联容器使用less<key_type>对元素排序,因此我们可以定义一个指针的set或者在map使用指针作为关键值而无须声明less
14.8.3 可调用对象与function
可调用对象有:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类
可调用对象也有类型,例如lambda有唯一的类类型,函数及函数指针的类型则由其返回值的类型和实参类型决定
两个不同类型的可调用对象可能共享同一种调用形式。调用形式指明了调用返回的类型以及传递给调用的实参类型。一个调用类型对应一个函数的类型:
int(int, int)
//是一个函数类型,接受两个int、返回一个int
不同类型可能具有相同的调用形式
对于几个可调用对象共享同一种调用形式的情况,有时我们会希望把它们看成具有相同的类型:
int add(int i, int j) { return i + j; }
auto mod = [](int i, int j) {retrun i % j; };
//函数对象类
struct divide{
int operator()(int denominator, int divisor){
return denominator / divisor;
}
}
这些可调用对象分别对齐参数执行了不同的运算,类型也不同,但是共享同一种调用形式:
int(int, int);
我们可能希望用这些函数构建一个简单的桌面计算器,需要定义一个函数表用于存储执行这些可调用对象的“指针”。当程序要执行某个特定的操作时,从表中查找该调用的函数
c++中用map来实现。此例中使用一个表示运算符符号的string对象作为关键字;使用实现运算符的函数作为值。当我们需要求给定运算符的值时,先通过运算符索引map,然后调用找到这个元素
假定所有函数相互独立,并且只处理关于int的二元运算,则map可以定义成如下形式:
map<string, int(*)(int, int)> binops;
//可以把add指针添加进去
binops.insert({"+", add});
//不能将mod或者divide存入binops
binops.insert({"%", mod}); //错误
标准库function类型
可以使用function的新标准库解决上面的问题,操作见表14.3
function是一个模板,当创建一个具体function类型时我们必须提供额外信息。
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){ return i* j;};
cout << f1(4,2) <<endl; //6
cout << f2(4,2) <<endl; //2
cout << f3(4,2) <<endl; //8
使用这个function可以重新定义map
map<string, function<int(int, int)>> binops;
就能把所有的可调用对象添加到这个map中
map<string, function<int(int, int)>> binops = {
{"+", add},
{"-", std::minus<int>()},
{"/", divide()},
{"*", [](int i, int j){ return i* j;}},
{"%", mod};
}
索引binops可以得到关联值的引用。
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}); //错误,哪个add?
解决二义性的一个途径是存储函数指针而非函数名字
int (*fp)(int, int) = add;
binops.insert({"+", fp});
或者用lambda来消除二义性
binops.insert( {"+", [](int a, int b) {return add(a,b); } } );
新版本 的function和旧版本的unary_function和binary_function没有关联,后两个类以及被更通用的bind函数替代了
14.9 重载、类型转换与运算符
我们同样能定义对于类类型的类型转换,通过定义类型转换运算来实现。转换构造函数和类型转换运算符共同定义了类类型转换,也被称为用户定义的类型转换
14.9.1 类型转换运算符
是类的一种特殊成员,负责将一个类类型的值转换成其它类型,一般形式如下:
operator type() const;
其中tpye表示某种类型。类型转换运算符可以面向任意类型(除了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对象转换成int:
SmallInt si;
si = 4;
si + 3; //先把si转成int,然后执行加法
编译器一次只能执行一个用户定义的类型转换,但隐式的用户定义可以置于一个标准类型转换之前或之后,并一起使用。因此可以将任何算术类型传递给SmallInt的构造函数。
//内置类型转换将double转为int
SamllInt si = 3.14;
si + 3.14; //内置转换类型将所得int转成double
因为式隐式执行的,所以无法传参。同时尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值:
class SmallInt;
operator int(SmallInt&); //错误,不是成员函数
class SmallInt{
public:
int operator int() const; //错误,指定了返回类型
operator int (int = 0) const; //错误,参数列表不为空
operator int*() const {return 42;} //错误,42不是指针
}
提示:避免过度使用类型转换函数,如果类类型转换不存在明显关系,会存在误导性。
类型转换运算符可能产生意外结果
实践中类很少提供类型转换运算符,只有一种例外:对于类来说,定义向bool的类型转换比较普遍
在早期版本,如果定义一个向bool的类型转换,那么就能被用到任何需要算术类型的上下文中,可能会引发错误,特别是istream含有向bool的类型转换
//可以通过编译
int i = 42;
cin << i; //如果向bool的类型转换不是显式的,则该代码在编译器看来是合法的
试图将输出运算符作用于输入流。因为instream本身没有定义<<。此代码会把cin转换成bool,在提升为int,用左移运算符。这样一来提升后的bool值最终会被左移42个位置
显式的类型转换运算符
防止这样的异常,C++11引入了显式的类型转换运算符
class SmallInt{
public:
//编译器不会自动执行这一类型转换
explicit operator int() const {return val;}
}
和显式构造函数一样,编译器不会将一个显式的类型转换运算符作用与隐式类型转换
SmallInt si = 3; //正确
si + 3; //错误
static_cast<int>(si) + 3; //正确,显式地请求类型转换
该规定存在一个例外,即如果表达式被用作条件,则编译器会将显式地类型转换自动应用于它,即以下情况:
- if、while和do语句地部分
- for语句头地添加表达式
- 逻辑! || &&
- 条件运算符(? :)的条件表达式
转换为bool
早期版本IO类型定义了向void*的转换规则避免了问题,C++11下IO标准库通过定义一个向bool的显式转换类型实现同样的目的
无论在什么条件下使用流对象,都会使用为IO类型定义的operator bool:
while(std::cin >> value)
向bool的类型转换通常用在条件部分,因此operator bool一般定义为explicit的
14.9.2 避免有二义性的类型转换
如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式。否则会存在二义性
两种情况可能产生多重转换路径:
- 两个类提供相同的类型转换,如A定义了接受B类对象的转换构造函数,B类定义了一个转换目标是A类的类型转换运算符。我们说它们提供了相同的类型转换
- 定义了多个转换规则,而这些转换涉及的类型本身可以通过其它类型转换联系在一起。如算术运算符,对某个给定的类来说,最好只定义最多一个于算术类型有个的转换规则
实参匹配和相同的类型转换
例:定义了两种B转换成A的方法:一种使用类型转换运算符、另一种使用A的以B为参数的构造函数
struct B;
struct A{
A() = default;
A(const b&); //把一个B转换A
}
struct B{
operator A() const; //也是把B转换成A
}
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;
operaotr double() const;
}
void f2(long double);
A a;
f2(a); //二义性错误,是int还是double
long lg;
A a2(lg); //二义性错误
编译器没法区分先转换int还是double,会产生二义性
如果转换过程中包含标准类型的转换,则标准类型转换的级别决定编译器选择最佳匹配的过程
short s = 42;
A a3(s); //使用A::A(int)
优先把short转换为int
重载函数与转换构造函数
当调用了重载的函数时,从多个类型转换中进行选择会更加复杂。如果两个或多个类型转换都提供了同一种可行匹配,则这些类型转换一样好。
例:当几个重载函数的参数分属不同的类类型时,如果恰好定义了同样的转换构造函数,二义性问题会进一步提升:
struct C{
C(int);
}
struct D{
D(int);
}
void manio(const C&);
void manio(const D&);
manio(10); //二义性错误
调用者可以显式地构造正确地类型从而消除二义性
manip(C(10));
重载函数与用户定义地类型转换
同理,调用manip时,即使其中一个类定义了需要对实参进行标准类型转换的构造函数,还是会有二义性
struct E{
E(double);
};
void manio2(const C&);
void manio2(const E&);
manio2(10); //二义性错误,含义是C(10)还是D(double(10))
14.9.3 函数匹配和重载运算符
通用的函数匹配规则同样适用于判断在给定的表达式种到底应该使用内置运算符还是重载运算符
如果a是一个类类型,则表达式a sym b可能是:
a.operatorsym(b);
operatorsym(a, b);
我们不能通过调用的形式来区分是否是成员函数还是非成员函数
表达式中运算符的候选函数集既应该包括成员函数,也应该包括非成员函数
class SamllInt{
friend
SmallInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int = 0); //转换源为int的类型转换
operator int() const { return val; } //转换目标为int的类型转换
private:
std::size_t val;
}
试图执行混合模式的算术运算就会有二义性问题
SamllInt s1, s2;
SmallInt s3 = s1 + s2; //使用重载的operator+
int i = s3 + 0; //二义性错误
因为我们可以把0转换成SmallInt,然后+。也可以把s3转成int,然后两个int执行内置+
如果我们对同一个类既提供了转换目标是算术类型的运算,又提供了重载的运算符,则会遇到重载运算符和内置运算符的二义性问题