14.1 基本概念

  • 重载的运算符是特殊的函数:名字由关键字operator后接要定义的算符共同组成,也有返回类型、参数列表、函数体。
  • 重载运算符函数的参数量与该算符作用的运算对象数量一样多
  • 除重载调用运算符operator()外,其他重载运算符不能有默认实参
  • 若一个重载运算符是成员函数,则其第一个运算对象(左侧对象)隐式绑定到该类的this指针上。故成员函数的显式参数数量比运算对象数量少1
  • 不可重载内置类型的运算符。即,重载运算符或者是类的成员,或者至少有一个类类型参数
  • 只能重载已有的运算符,不可发明新的算符
  • +-*&这4个既可是一元算符也可是二元算符,都可被重载,根据参数数量判断是哪种。
  • 重载运算符的优先级结合律与对应的内置运算符一致,但求值顺序短路求值不会被保留

直接调用一个重载的运算函数

  • 可用运算符形式调用重载运算符,也可直接用函数名
//对于非成员函数运算符,下两行等价
data1+data2;
operator+(data1,data2); //对于成员函数运算符,下两行等价
data1+=data2;
data1.operator+=(data2);

某些运算符不应该被重载

  • 不建议重载的运算符:

    • &&||,的求值顺序不会被保留(因为算符重载本质是函数调用)
    • &&||的短路求值不会被保留
    • ,&这两种运算符对类类型对象有特殊含义,不建议重载

使用与内置类型一致的含义

  • 如果某些操作在逻辑上与算符相关,适合重载:

    • 执行IO操作的类,重载<<>>以和内置类型的IO操作一致
    • 检查相等性的类,重载==,同时也需重载!=
    • 单序比较操作的类,重载<,同时也需重载<=>>=
    • 重载运算符的返回类型通常应与内置版本兼容:逻辑运算符和相等运算符返回bool,算术运算符返回类类型的值,赋值运算符和复合赋值运算符返回左侧对象的引用

赋值和复合赋值运算符

  • 如果类重载了算术运算符或位运算符,则最好也提供对应的复合赋值运算符

选择作为成员或者非成员

  • 定义成员函数还是非成员函数的准则

    • =[]()>必须是成员
    • 复合赋值运算符一般是成员,但非必须
    • 改变对象状态的运算符或与给定类型密切相关的运算符(如++-、``等)通常应该是成员
    • 具有对称性的运算符(如算术、相等性、关系、位)可能转换任意一侧的对象,通常不应该是成员
  • 把运算符定义为成员函数时,其左侧对象必须是该类的一个对象
string s="world";
string t=s+"!"; //对,左侧是string对象
string u="hi"+s; //若operator+是string的成员函数且未定义非成员版本,则错

14.2 输入和输出运算符

  • IO库用>>和<<来执行输入和输出,IO库定义了读写内置类型的版本,类需自定义它们以支持IO

14.2.1 重载输出运算符 <<

  • 通常输出运算符的参数和返回值:

    • 第一个参数是非常量ostream对象的引用。非常量是因为向流写内容会改变流,引用是因为ostream不可拷贝
    • 第二个参数一般是常量引用。常量是因为打印不改变对象内容,引用是因为避免拷贝
    • 返回值是它的ostream形参的引用(以便重复打印)
  • IO运算符左侧是iostream,故它们必须是非成员函数(iostream类型不可被用户改变)
  • IO运算符经常要读写类的非公有成员,故一般声明为友元
//第一个参数是非常量ostream对象的引用,
//第二个参数一般是常量引用,
//返回值是它的ostream形参的引用
ostream &operator<<(ostream &os,const Sales_data &item){
os<<item.isbn()<<" "<<item.units_sold<<" "
<<item.revenue<<" "<<item.avg_price();
return os; //返回传入的ostream(以便重复打印)
}

14.2.2 重载输入运算符 >>

  • 通常输入运算符的参数和返回值:

    • 第一个参数是要读取的非常量istream对象的引用。非常量是因为读取也会改变流的状态
    • 第二个参数是将读入的非常量对象的引用。非常量是因为要写入该变量,引用也是因为要写入该变量
    • 返回值是它的istream形参的引用(以便重复读取)

输入时的错误

  • 输入运算符必须处理输入流可能无效的情况(因为输入会改变程序中的变量,无效流导致读入未定义的数据),而输出运算符不需要

    • 流含有错误类型的数据时读取操作可能失败
    • 读取操作到达文件末尾或遇到输入流的其他错误时也会失效
  • 检查输入流是否有效应该在读完后使用前检查,不应在读之前检查(否则无法检查读取过程中发生的错误)
  • 若在发现错误时对象已经被输入流改变,则改变部分是未定义的值。需要将对象重置为合法的(确定的)状态。
  • 重载输入运算符中读到了不符合用户需求的格式时,通常情况下,输入运算符只设置failbit。除此之外,设置eofbit表示文件耗尽,设置badbit表示流破坏。最好的方式时由IO标准库自己来标示这些错误。
//第一个参数是要读取的istream的引用
//第二个参数是将读入的非常量对象的引用
//返回值是它的istream形参的引用
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; //返回传入的istream(以便重复读取)
}

14.3 算术和关系运算符

  • 通常将算术和关系运算符定义为非成员,以允许对左侧或右侧的变量做类型转换。
  • 参数和返回值:
    • 形参都是常量引用,因为不需要改变对象状态
    • 返回值是非引用,经常是在函数内定义局部变量作为结果返回
  • 若类定义了算术运算符,一般也应该定义复合赋值运算符,且应该用复合赋值运算符实现算术运算符(算术算符比复合赋值算符多一次拷贝
Sales_data operator+(Sales_data &lhs,Sales_data &rhs){
Sales_data sum=lhs; //拷贝初始化
sum+=rhs;
return sum; //局部变量传出时拷贝
}

14.3.1 相等运算符

  • 类通过相等运算符来检验两个对象是否相等
  • 相等运算符的设计准则:
    • 定义相等运算符可更容易地使用标准库容器/算法
    • 若类定义了==,则可判断一组对象中是否有重复的
    • 相等运算符应该有传递性,即由a==bb==c可推出a==c
    • 定义了==的类也应该定义!=
    • ==!=中的一个应该把工作委托给另外一个(可保证两个对象不是相等就是不等)
    • ==!=应该返回bool类型
//参数为常量引用,返回值为bool
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==实现
bool operator!=(const Sales_data &lhs,const Sales_data &rhs){
return !(lhs==rhs); //委托给operator==
}

14.3.2 关系运算符

  • 定义了相等运算符的类经常(但非必须)定义关系算符
  • 关联容器和一些算法会用到operator<,它比较有用
  • 关系运算符的设计:
    • 定义顺序关系,这种关系与关联容器中对关键字的要求一致
    • 若类也有==,则定义的关系应与==保持一致。如果两个对象时!=的,那么一个对象应该<另外一个
    • 如果不存在一种逻辑可靠的<定义,则不定义<更好

14.4 赋值运算符

  • 拷贝赋值运算符移动赋值运算符外,类还可定义其他赋值运算符以使用其他类型作为右侧对象。但不需要考虑自赋值问题,因为类型不同。
  • 赋值运算符必须定义为成员函数,无论形参类型是什么。复合赋值运算符也通常(不必须)定义为成员
  • 赋值运算符和复合赋值运算符都应返回左侧对象的引用(对成员函数而言是this
class StrVec{
public:
StrVec &operator=(initializer_list<string>);
/* 其他成员与13.5一致 */
}; //重载赋值算符,通过initializer_list实现列表初始化
StrVec &StrVec::operator=(initializer_list<string> il){
auto data=alloc_n_copy(il.begin(),il.end()); //分配空间并将右侧对象的元素拷贝进去
free(); //释放左侧对象原来管理的资源
elements=data.first; //用左侧对象的指针管理拷贝的右侧对象
first_free=cap=data.second;
return *this //返回左侧对象的引用
}
StrVec v;
v={"a","an","the"}; //列表初始化(传入列表,调用参数为initializer_list的赋值算符)

14.5 下标运算符

  • 容器类经常可通过元素的位置来访问元素,这些类一般需要下标运算符[]
  • 下标运算符必须是成员函数
  • 参数和返回值:
    • 通常返回所访问元素的引用,这样可允许下标操作出现在=的左侧或右侧
    • 通常定义const和非const两个版本,作用于const对象的版本其返回值也应该是const
class StrVec{
public:
//非const版本的[],可赋值
string &operator[](size_t n){return elements[n];}
//const版本的[],不可赋值,不可改变对象。重载的原因不是返回值const而是后面的const限定符
const string &operator[](size_t n) const {return elements[n];}
/* 其他成员与13.5一致 */
private:
string *elements; //指向动态数组首元素的指针
};
StrVec svec; //非const对象
const StrVec cvec=svec; //const对象
if(svec.size() && svec[0].empty()){
svec[0]="zero"; //对,对非const对象调用非const版本的[],左侧是元素的引用
cvec[0]="zip"; //错,对const对象调用const版本的[],左侧是元素的常量引用
}

14.6 递增和递减运算符

  • 迭代器类通常需要实现++-,使迭代器类可在元素序列中前后移动。
  • 递增递减运算符建议(非必须)设为成员函数,因为它们改变操作对象的状态
  • 递增递减运算符同时存在前置版本和后置版本,它们应该被同时定义。
    • 前置版本实现递增/递减操作,并返回对象本身(返回引用
    • 后置版本实现递增/递减操作,并返回操作前的对象的拷贝(返回值)(需在操作前就拷贝)
  • 前置版本和后置版本名字相同无法区分(返回类型不同不算重载),因此使后置版本接受一个额外的int形参以区分,该形参的唯一作用是区分前置/后置因此不需命名,使用后置时编译器为它自动赋值为0
  • 后置版本只是比前置版本多一次拷贝且返回值不同,故应该用前置版本来实现后置版本
class StrBlobPtr{
public:
StrBlobPtr &operator++();
StrBlobPtr &operator--();
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
/* 其他成员与12.1.6中一致 */
}; //前置版本++,包含越界检查
StrBlobPtr &StrBlobPtr::operator++(){
check(curr,"increment past end of StrBlobPtr");
++curr; //先判断是否>=size再递增,因为右边界是开区间
return *this;
} //前置版本--,包含越界检查
StrBlobPtr &StrBlobPtr::operator--(){
--curr; //先递减再判断是否>=size(size_t下溢),因为左边界是闭区间
check(curr,"decrement past begin of StrBlobPtr");
return *this;
} //后置版本++,委托前置++来实现
StrBlobPtr StrBlobPtr::operator++(int){
StrBlobPtr ret=*this; //先拷贝一份用于返回
++*this; //将本对象++的实现委托给前置版本
return ret; //返回改变前的拷贝
} //后置版本--,委托前置--来实现
StrBlobPtr StrBlobPtr::operator--(int){
StrBlobPtr ret=*this; //先拷贝一份用于返回
--*this; //将本对象--的实现委托给前置版本
return ret; //返回改变前的拷贝
} //可显式调用重载的递增/递减。调用后置版本时需给int传参
StrBlobPtr p(a1);
p.operator++(0); //调用后置版本
p.operator++(); //调用前置版本

14.7 成员访问运算符

  • 迭代器类或智能指针类中常用到解引用运算符*和箭头运算符->
  • ->必须是成员函数,*经常也是成员函数(非必须)
  • *->是作用在指针上,访问底层对象,不改变指针本身,因此常定义为const函数
  • 理论上可让*做任何事,但->只能用于获取成员。且->的工作经常委托给*来实现
  • 对于表达式point->mem,point必须是指向类对象的指针,或是重载了->的对象:
    • 若point是指向类对象的指针,则等价于(*point).mem
    • 若point是重载了->的对象,则等价于point.operator->()->mem
  • 因此operator->()应该返回一个指向类对象的指针,或是另一个重载了->的对象
class StrBlobPtr{
public:
//返回string的引用
string &operator*() const {
auto p=check(curr,"dereference past end");//若未越界则返回指向底层对象的shared_ptr
return (*p)[curr]; //在底层对象中随机索引,返回索引到的元素
}
//返回指向string的指针
string *operator->() const{
return &this->operator*(); //对operator*的返回对象取地址得到指针
}
/* 其他成员与12.1.6中一致 */
}; //使用
StrBlob a1={"hi","bye","now"};
StrBlobPtr p(a1);
*p="okay"; //解引用得到的是左值引用,可赋值
cout<<p->size()<<endl; //结果是4,等价于p.operator->()->size()
cout<<(*p).size()<<endl; //结果是4,等价于p.operator*().size()

14.8 函数调用运算符

  • 若类重载了函数调用运算符,则可以像使用函数一样使用类的对象,它比普通函数更灵活
  • 函数调用运算符必须是成员函数。一个类可定义多个调用算符,相互之间应在参数数量/类型上区分
  • 若类定义了调用算符,则称该类对象为函数对象
  • 函数对象经常用作泛型算法的实参,如用标准库for_each算法和PrintString类打印容器内容
class PrintString{
public:
//构造函数,指明打印对象和打印分隔符
PrintString(ostream &o=cout,char c=' '):
os(o),sep(c)
{}
//调用算符,进行实际的打印操作
void operator()(cosnt string &s) const {os<<s<<sep;}
private:
ostream &os;
char sep;
}; //使用函数对象
PrintString printer; //定义默认的函数对象
printer(s); //调用函数对象
PrintString errors(cerr,'\n'); //定义函数对象
errors(s); //调用函数对象 for_each(vs.beign(), vs.end(), PrintString(cerr,'\n'))

14.8.1 lambda是函数对象

  • 编写lambda后,编译器将lambda翻译为一个未命名类的未命名对象。
  • lambda产生的匿名类中含有一个重载的函数调用运算符,且形参列表和lambda的形参列表一样
  • 默认lambda不可改变捕获的变量,故其中重载的调用算符是const函数。除非将lambda声明为mutable
//法1:用lambda实现谓词
stable_sort(words.begin(),words.end(),
[](const string &a,const string &b){return a.size()<b.size();});
//法2:用等价的函数对象实现谓词
class ShorterString{
public:
//调用算符的返回值、形参列表、函数体都与lambda一致
bool operator()(const string &a,const string &b) const {return a.size()<b.size();}
};
stable_sort(words.begin(),words.end(),ShorterString())

表示 lambda 及相应捕获行为的类

  • 两种捕获:

    • 引用捕获:编译器直接使用,不需存储数据成员。程序负责保证引用的对象存在
    • 值捕获:对象被拷贝到lambda中,在生成的函数对象中为值捕获的变量建立成员,同时创建构造函数用初值列表初始化它们
  • lambda产生的类不含默认构造函数、赋值算符、默认析构函数,它是否含有默认拷贝/移动构造函数通常取决于捕获的数据成员类型
//值捕获的lambda
auto wc=find_if(words.begin(),words.end(),
[sz](const string &a){return a.size()>=sz;}); //lambda 表达式产生的类形如
class SizeComp{
public:
//构造函数用于初始化变量
SizeComp(size_t n):sz(n){}
//调用算符
bool operator()(const string &a) const {return a.size()>=sz;}
private:
//为捕获的值生成数据成员
size_t sz;
}; //使用时用值捕获的变量初始化函数对象
auto wc=find_if(words.begin(),words.end(),SizeComp(sz));

14.8.2 标准库定义的函数对象

  • 标准库定义了一组表示算术、关系、逻辑算符的类,每个类分别定义了一个执行命名操作的调用算符
  • 这些类都是模板,可为其指定应用的类型,即调用该算符的形参类型
plus<int> intAdd;               //int的加法
negate<int> intNegate; //int的取反
int sum=intAdd(10,20); //结果是30
sum=intNegate(intAdd(10,20)); //结果是-30
sum=intAdd(10,intNegate(10)); //结果是0

在算法中使用标准库函数对象

  • 表示算符的函数对象类常用于替换标准库算法中的默认算符(类似lambda)
  • 直接用算符比较指针是未定义,但可用标准库的函数对象来比较指针
vector<string *> nameTable;

//用内置的<对string指针排序,未定义
sort(nameTable.begin(),nameTable.end(),
[](string *a,string *b){return a<b;}); //用标准库定义的函数对象对string指针排序,正确
sort(nameTable.begin(),nameTable.end(),less<string *>());

14.8.3 可调用对象与function

  • C++中可调用对象的种类:函数函数指针lambda表达式bind创建的对象重载了函数调用算符的类
  • 调用形式指明了调用返回的类型以及传递给调用的参数类型,一种调用形式对应一个函数类型。不同类型的可调用对象可共享同一种调用形式
  • 有时候希望将共享调用形式的可调用对象看成同一种类型的对象。例如定义函数表用于存储指向一些可调用对象的指针,程序需要某个可调用对象时从表中查找该对象
  • 函数表可用map实现,key是string,value是可调用对象的指针。但这些指针类型各不相同,无法确定map的类型
//以下3个可调用对象的调用形式都是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;}
}; map<string,int(*)(int, int)> binops;
binops.insert({"+", add}); //正确,add是函数指针
binops.insert({"%", mod}); //错误,mod是函数指针

标准库 function 类型

  • 使用名为function的标准库类型可将调用形式相同的不同类型可调用对象统一表示,它定义于functional头文件

  • function是模板类,其模板参数是它能表示的调用形式(对应的函数类型)

  • 可将不同类型的可调用对象存入同类型的function中,只要它们调用形式相同

  • function类型重载了调用算符,该算符将其接受的实参传递给底层的可调用对象

/* 上下文:上一个例子 */
function<int(int,int)> f1=add; //加法,函数
function<int(int,int)> f2=divide(); //除法,重载调用算符的类
function<int(int,int)> f3=[](int i,int j){return i*j}; //乘法,lambda实现
cout<<f1(4,2)<<endl; //6
cout<<f2(4,2)<<endl; //2
cout<<f3(4,2)<<endl; //8 //定义函数表,用于查找操作并调用。统一类型为function<int(int,int)>
map<string,function<int(int,int)>> binops={
{"+",add}, //上文定义的函数
{"-",minus<int>()}, //标准库的函数对象
{"/",divide()}, //上文定义的重载调用算符的类
{"*",[](int i,int j){return i*j;}}, //lambda
{"%",mod}, //上文定义的lambda
}; //使用函数表
binops["+"](10,5); //调用add(10,5)
binops["-"](10,5); //使用minus<int>对象的调用算符
binops["/"](10,5); //使用divide对象的调用算符
binops["*"](10,5); //使用lambda对象
binops["%"](10,5); //使用lambda对象

重载的函数与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}); //错,function不能识别是哪个重载函数
int (*fp)(int,int)=add; //对,函数指针能识别是哪一个重载函数
binops.insert({"+",fp}); //对,直接调用也能识别是哪一个重载函数
binops.insert({"+",[](int a,int b){return add(a,b);}});

14.9 重载、类型转换与运算符

用户定义的类型转换:

  • 转换构造函数:由一个实参调用的非explicit构造函数定义类型转换,将实参类型转换为该类类型
  • 类型转换算符:重载的算符,将该类类型转换为指定类型

14.9.1 类型转换运算符

  • 类型转换算符是一种特殊的成员函数,负责将该类类型转换为其他类型。
  • 类型转换算符的定义形式为operator type() const,其中type表示要转换为的类型。
  • 类型转换算符可对除void外的任何类型定义,只要该类型可作为函数的返回类型(因为用return实现)。故不转换为数组或函数,但可转换为它们的指针/引用
  • 类型转换算符没有显式的返回类型,也没有形参,且必须定义为成员函数。通常不应该改变原对象的内容(拷贝而非移动),故经常定义为const
  • 编译器一次只能执行一个自定义的类型转换,但一个隐式的自定义类型转换可与一个隐式的内置类型转换一起使用。
  • 类型转换隐式执行,故类型转换算符都没有形参
  • 类型转换算符的返回类型不需要指定(蕴含在函数名中)
class SmallInt{
public:
//构造函数实现int向该类的转换
SmallInt(int i=0):val(i){
if(i<0 || i>255)
throw out_of_range("Bad SmallInt value");
}
//类型转换算符实现该类向int的转换
operator int() const {return val;}
private:
size_t val;
};
SmallInt si;
si=4; //4隐式转换为SmallInt,调用合成的SmallInt::operator=
si+3; //si隐式转换为int,调用int的加法(因为SmallInt未重载opeartor+) //可同时使用一个隐式的自定义类型转换与一个隐式的内置类型转换
si=3.14; //先内置转换把3.14转为int,再调用转换算符转为SmallInt
si+3.14; //先调用转换算符把si转为int,再内置转换为double //错误示范
class SmallInt;
operator int(operator &); //错误,不是成员函数
class SmallInt{
public:
int operator int() const; //错误:制定了返回参数
operator int(int=0) const; //错误:参数列表不为空
operator int*() const; //错误,42不是一个指针 };

类型转换运算符可能产生意外结果

  • 若在类类型和要转换的类型之间没有明显的映射关系,最好不要定义转换算符
  • 实践中,类很少提供转换算符。隐式转换发生时用户可能感到意外而不是被帮助
  • 定义转为bool的转换算符比较普遍,经常用于条件判断场合
  • 为类定义转为bool的转换算符存在的问题是:bool可参与算术运算,可能引发意想不到的结果
/* 假设cin可以隐式转为bool */
int i=42;
cin<<i;
//cin未定义<<操作,但可转为bool
//于是cin转为bool,无效为0,有效为1。<<成为左移操作,将cin提升为int
//结果是将0或1左移42bit

显示的类型转换运算符

  • 防止上述异常,C++11引入显式类型转换算符,即在类型转换算符前使用explicit关键字,只允许用强制转换方式实现类型转换
  • 若表达式用作条件,则编译器将显式的类型转换自动应用于该表达式(即忽略explicit):
    • if/while/do语句的条件部分
    • for语句头的条件表达式
    • 逻辑非!、逻辑与&&、逻辑或||的运算对象
    • 条件算符?:的条件表达式

转换为 bool

  • 实现转为bool但避免算术运算的方法:

    • C++11中:将向bool转换的转换算符定义为explicit。变量单独放于条件中会自动忽略explicit
    • 旧标准中:定义向void *而非bool的转换算符。void *也可用于判断,但不会参与算术运算
  • operator bool经常被定义为explicit的

14.9.2 避免有二义性的类型转换

  • 若类中包含一个或多个类型转换,则必须确保在该类类型和目标类型之间只存在唯一的转换方式,即无二义性
  • 两种情况下可能产生二义性
    • 两个类定义了作用相同的类型转换,例如:A定义了接受B类型的转换构造函数,同时B定义了向A转换的转换算符
    • 类定义了多个转换算符,而这些转换目标的类型又能相互转换,例如:类定义了转换为int的算符,又定义了转换为short的算符,而实际需要一个double类型

实参匹配和相同的类型转换

  • 不要为多个类定义相同的类型转换,不要为类定义多个转换源或转换目标是算术类型的转换
  • 对于多个类定义相同类型转换的二义性,解决方式是显式调用函数。显式转换无法解决,因为显式转换也面临二义性
struct B;
struct A{
A()=default;
A(const B &); //B转为A
};
struct B{
operator A() const; //B转为A
};
//使用这两个类
A f(const A &); //函数接受A的引用,传入B的对象时发生隐式转换
B b;
A a=f(b); //二义性错误:B转为A时,可调用A的构造函数,也可调用B的转换算符
//解决方案:显式调用
A a1=f(b.operator A()); //对,显式调用B的转换算符
A a2=f(A(b)); //对,显式调用A的构造函数

二义性与转换目标为内置类型的多重类型转换

  • 对于类定义了多个转换算符,而这些转换目标的类型又能相互转换的情形,二义性的根本原因是它们所需的内置转换级别一致

  • 使用自定义的转换算符时,若转换过程包含内置转换,则内置转换的级别决定选择哪个转换算符作为最佳匹配

    struct A{
    A(int=0); //int转为A
    A(double); //double转为A
    operator int() const; //A转为int
    operator double() const; //A转为double
    };
    //使用A
    void f2(long double);
    A a;
    f2(a); //二义性,f2接受long double,存在两条转换路径:A->int->long double,A->double->long double
    long lg;
    A a2(lg); //二义性,转换为A类型时有两条转换路径:long->int->A,long->double->A
    short s=42;
    A a3(s); //无二义性,"short->int"优于"short->double",故选择转换路径:short->int->A

类型转换与运算符

  • 设计类的重载算符、转换构造函数、类型转换函数时,必须小心。类同时定义了类型转换算符和重载算符时很容易二义性。

    • 不要让两个类实现相同的类型转换
    • 避免转换目标是内置算术类型的转换算符
      • 定义了转为算术类型的算符时,不要再定义接受算术类型的重载算符
      • 定义了转为算术类型的算符时,不要定义转换到多种算术类型的转换算符
    • 除了explicit地转换为bool的算符外,应尽量避免定义类型转换算符,并尽可能限制那些“显然正确”的非explicit构造函数

重载函数与转换构造函数

  • 调用重载函数时,从多个类型转换中进行选择将更加复杂。若两个或多个类型转换都提供了同一种可行的匹配,则这些转换一样好
  • 调用者可显式构造正确的类型消除二义性
  • 若在调用重载函数时,需要使用显式构造函数强制类型转换来改变实参类型,通常意味着程序设计存在不足
struct C{
C(int); //int转为C
};
struct D{
D(int); //int转为D
};
void manip(const C &);
void manip(const D &);
manip(10); //二义性错误,manip(C(10))或manip(D(10))
manip(C(10)); //正确,显式调用

重载函数与用户定义的类型转换

  • 调用重载函数时,若两个或多个自定义类型转换都提供了可行匹配,则它们提供的转换一样好。此时不考虑任何内置类型转换的级别。
  • 只有所有可行函数能通过同一个自定义类型转换得到匹配时,才会考虑其中出现的内置类型转换
  • 若调用重载函数所请求的自定义类型转换不止一个,即使其中一个需要额外的内置类型转换而另一个精确匹配,它们也是二义性的。(本质原因是,对于自定义类型转换之间的差异而言,内置类型转换的差异可以忽略
struct C{
C(int); //int转为C
};
struct E{
E(double); //int转为E
};
void manip2(const C &);
void manip2(const E &);
manip2(10); //二义性,可匹配到两个类型转换:int->C和int->double->E
//但由于是重载函数且是不同的自定义转换,故它们一样好

14.9.3 函数匹配与重载运算符

  • 重载的算符也是重载的函数,故通用的函数匹配规则也适用于判断表达式中使用哪个算符。
  • 算符出现在表达式中时,候选函数集会比较大,例如a sym b可能是:
    • 内置算符sym
    • 重载的非成员函数,即operatorsym(b)
    • 重载的a的成员函数,即a.operatorsym(b)(当a出现在左侧时才有可能)
  • 显式调用一个命名的函数时,具有该名字的成员函数和非成员函数不会彼此重载,因为会用.>来显式说明是成员。但算符无法被这样区分。
  • 若对同一个类,既提供了转换源和转换目标是算术类型的转换,又提供了重载的算符,则会遇到重载算符内置算符的二义性问题
class SmallInt{
//对SmallInt重载+
friend SmallInt operator+(const SmallInt &,const SmallInt &);
public:
SmallInt(int=0); //int转为SmallInt
operator int() const {return val;} //SmallInt转为int
private:
size_t val;
};
SmallInt s1,s2;
SmallInt s3=s1+s2; //两侧都是SmallInt,使用重载的operator+
int i=s3+0; //一侧SmallInt,另一侧int
//但既可由int转为SmallInt,又可由SmallInt转为int
//且存在int的+和SmallInt的operator+
//故无法选择,二义性错误

【c++ Prime 学习笔记】第14章 重载运算与类型转换的更多相关文章

  1. C++ Primer 5th 第14章 重载运算与类型转换

    当运算符作用域类类型的对象时,可以通过运算符重载来重新定义该运算符的含义.重载运算符的意义在于我们和用户能够更简洁的书写和更方便的使用代码. 基本概念 重载的运算符是具有特殊名字的函数:函数名由关键词 ...

  2. thinking in java学习笔记:14章 类型信息

    14.2 Class 对象 https://github.com/zhaojiatao/javase 1.什么是Class对象,Class对象是用来做什么的? Class对象是java程序用来创建类的 ...

  3. 《机器学习实战》学习笔记——第14章 利用SVD简化数据

    一. SVD 1. 基本概念: (1)定义:提取信息的方法:奇异值分解Singular Value Decomposition(SVD) (2)优点:简化数据, 去除噪声,提高算法的结果 (3)缺点: ...

  4. [C++ Primer] : 第14章: 重载运算符与类型转换

    基本概念 重载运算符是具有特殊名字的函数: 它们的名字由关键字operator和其后要定义的运算符号共同组成. 重载运算符函数的参数数量与该运算符作用的运算对象数量一样多. 对于二元运算符来说, 左侧 ...

  5. C++ Primer : 第十四章 : 重载运算与类型转换之重载运算符

    重载前须知 重载运算符是特殊的函数,它们的名字由operator和其后要重载的运算符号共同组成. 因为重载运算符时函数, 因此它包含返回值.参数列表和函数体. 对于重载运算符是成员函数时, 它的第一个 ...

  6. 【c++ Prime 学习笔记】目录索引

    第1章 开始 第Ⅰ部分 C++基础 第2章 变量和基本类型 第3章 字符串.向量和数组 第4章 表达式 第5章 语句 第6章 函数 第7章 类 第 Ⅱ 部分 C++标准库 第8章 IO库 第9章 顺序 ...

  7. Stealth视频教程学习笔记(第一章)

    Stealth视频教程学习笔记(第一章) 本文是对Unity官方视频教程Stealth的学习笔记.在此之前,本人整理了Stealth视频的英文字幕,并放到了优酷上.本文将分别对各个视频进行学习总结,提 ...

  8. Programming Entity Framework-dbContext 学习笔记第五章

    ### Programming Entity Framework-dbContext 学习笔记 第五章 将图表添加到Context中的方式及容易出现的错误 方法 结果 警告 Add Root 图标中的 ...

  9. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二章:矩阵代数

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二章:矩阵代数 学习目标: 理解矩阵和与它相关的运算: 理解矩阵的乘 ...

随机推荐

  1. shell循环语句until

    until 条件 do 执行命令 done until 循环与 while 循环在处理方式上刚好相反. 当条件成立的时候,停止循环.

  2. 20210821 打表,蛇,购物,ants

    考场 T1 没看懂 T4 一眼回滚莫队,但忘记怎么写了,小慌 模拟 T1 题意的时候教练让 zsy 澄清了一下,确定了我不会做... T2 一看就是毒瘤题,T3 感觉比较可做 T4 确定了回滚的细节, ...

  3. Appium问题解决方案(9)- Original error: Failed to launch Appium Settings app: Condition unmet after 5090 ms

    背景 执行代码报错 解决方法 该问题并不常见,主要是手机操作系统的问题 程序无法无法自动打开appiumsettings,那么我们可以手动打开appiumsettings服务(程序没有界面,会出现打开 ...

  4. Linux 网卡驱动sk_buff内核源码随笔

    这几天在调试有关网卡驱动的东西,有很多地方不清楚.有关网卡驱动部分主要有两个很重要的结构体:struct net_device 和struct sk_buff. 驱动大部分都是围绕这两个东西进行操作的 ...

  5. token过期时间

    token有两个有效期,在config/jwt.php下面 # ./config/jwt.php'ttl' => env('JWT_TTL', 600), ##600min=10h 'refre ...

  6. WEB漏洞——PHP反序列化

    序列化 首先说说什么是序列化 序列化给我们传递对象提供了一种简单的方法.serialize()将一个对象转换成一个字符串,并且在转换的过程中可以保存当前变量的值 而反序列化unserialize()将 ...

  7. ClientValidationFunction

    CustomValidator.ClientValidationFunction 属性 获取或设置用于验证的自定义客户端脚本函数的名称. 命名空间:   System.Web.UI.WebContro ...

  8. 分享一则Linux系统邮件提示 /usr/local/lib/libprocesshider.so > /etc/ld.so.preload 的中病毒解决方法

    ​ 系统环境:CentOS Linux release 7.6.1810 (AltArch)                   CPU架构:ARM            最近发现生产服务器CPU占用 ...

  9. python库--pandas--MultiIndex

    *表示后面会重复用到此参数 创建层次化索引 pd.MultiIndex 构造器 MI levels 每个级别不重复的标签 labels 每个级别的整数指定每个位置 *sortorder=None   ...

  10. STP(生成树)的概述和工作原理

    一.STP的简介 二.STP的概述 三.STP的工作原理 四.生成树的算法 五.BPDU(桥协议数据单元) 六.STP收敛 TP简介 STP - Spanning Tree Protocol(生成树协 ...