C++ Primer第14章 重载运算与类型转换


一、14.1 基本概念

重载的运算符是具有特殊名字的函数:它们的名字由关键字 operator和其后要定义的运算符号共同组成,与其他函数一样重载运算符也要包含返回值类型、参数列表以及函数体。
如果一个运算符函数是成员函数,则其他的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数的参数数量比运算符的运算对象总数要少一个;
对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数:

//错误:不能为int重定义内置的运算符
int operator+(int, int)

这一约定意味着当运算符作用于内置类型的运算对象时候,我们无法改变该运算符的含义。
我们只能重载已有的运算符,不能发明新的运算符,例如:不能提供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的成员,则会产生错误:  

第一个加法等价于: s.operator+("!");
同样的,“hi”.operator+(s),显然“hi”的类型是一个const char*,这是一种内置类型,根本没有成员函数!

二、14.2 输入和输出运算符

14.2.1 重载运算符<<

通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用,之所以ostream是非常量是因为向流中写入内容会改变其状态,而该形参是引用我们无法直接复制一个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标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数,否则,他们的左侧运算对象将是我们的类的对象:

Sales_data data;
data<<cout; //如果operator<<是Sales_data的成员

因此我们希望为类自定义IO运算符,则必须将其定义为非成员函数,当然,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;
}

输入时的错误
在执行输入运算符的时候可能会发生错误,在程序中我们没有逐个检查每个读取操作,而是读取了所有数据后赶在使用这些数据之前进行一次性检查:

if(is)   //检查输入是否成功
     item.revenue = item.units_sold * price;
else
     item = 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;    //把lhs的数据成员拷贝给sum
    sum += rhs;             //把rhs加到sum中
    return sum
}

14.3.1 相等运算符

通常情况下,C++中的类通过定义相等的运算符来比较两个对象是否相等,也就是说,他们会比较对象的每一个数据成员,只有全部相等时,我们才会认为两个对象对象相等。

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);
}

14.3.2 关系运算符

通常情况下关系运算符应该:
1.定义顺序关系,令其与关联容器中对关键字的要求一致;
2.如果类中同时含有== 运算符的话,则定义一种关系与其保持一致(==),特别是,如果两个对象是!=的,则一个对象应该<另一个。
注意:如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符,如果类同时包含 == ,则当且仅当<的定义和 ==产生的结果一致时才定义<运算符。

四、14.4 赋值运算符

类还可以定义其他赋值运算符已使用别的类型作为右侧运算对象

vector<string> v;
v = {"a", "an", "the"};

同时,也可以把这个运算符添加到类中:

class StrVec{
public:
   StrVec &operator=(initializer_list<string>);
   //其他成员与13.3节一致
}
StrVec &StrVec::operator=(initializer_list<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 &lhs) {
	units_sold += lhs.units_sold;
	revenue += lhs.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;  //指向数组首元素的指针
}

上面两个下标运算符的用法类似于vector或者数组中的下标,因为下标运算符返回的是元素的引用,所以当StrVec是非常量的时候,我们可以给元素赋值,当我们对常量对象取下标时,不能为其赋值。

//假设svec是一个StrVec对象
const StrVec cvec = svec;  //拷贝构造
//如果svec中含有元素,对第一个元素运行string的emety()函数
if (svec.size() && svec[0].empty()){
   svec[0] = "zero";  //正确;下标运算符返回string的引用
   cvec[0] = "zip";   //错误;对cvec取下标返回的是常量引用
}

六、14.6 递增和递减运算符

C++语言并不要求递增和递减运算符必须是类的成员,但是因为它们改变的是所操作对象的状态,所以建议将其设定为成员函数。
对于内置类型来说,递增和递减运算符既有前置版本,又有后置版本,同样,我们也应该为类定义两个版本的递增和递减运算符。
定义前置递增/递减运算符
为了说明递增和递减运算符,在StrBlobPtr(12.1.6节中定义)

class StrBlobPtr{
public:
    //递增和递减运算符
    StrBlobPtr& operator++();
    StrBlobPtr& operator--();
    //其他成员
};

工作机理:首先利用check函数检查StrBlobPtr是否有效,如果有效,接着检查给定的索引值是否有效,如果由异常抛出,则运算符返回对象的引用。
在这个例子中,我们把curr的当前值传递给check函数,如果这个值小于vector大小,则正常返回,否则,如果curr已经在vector末尾,check将抛出异常。

StrBlobPtr& StrBlobPtr::operator++(){
   //如果curr也已经指向了容器尾后位置,则无法递增
   check(curr, "increment past end of StrBlobPtr");
   ++curr;  //将curr在当前状态下向前移动一个元素
   return *this;
}
StrBlobPtr& StrBlobPtr::operator--(){
   //如果curr也已经指向了容器首位置,则递减后产生一个无效下标
   --curr;  //将curr在当前状态下向后移动一个元素
   check(curr, "decrement past begin of StrBlobPtr");
   return *this;
}

区分前置和后置运算符
前置和后置使用的是同一个符号,意味着重载版本使用的名字将是相同的,并且运算对象的数量和类型也是相同的,为了解决这个问题,后置版本接受一个额外的不被使用的int类型的形参,当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参。尽管从语法上说后置函数可以使用这个额外的形参,但是实际过程中一般不会这么做,这个形参的唯一作用就是区分前置和后置版本。

class StrBlobPtr{
public:
    //递增和递减运算符
    StrBlobPtr operator++(int);
    StrBlobPtr operator--(int);
    //其他成员  
}

注意:为了和内置版本保持一致,后置运算符返回的是对象的原值,返回的形式是一个值不是引用。

//后置递增版本
StrBlobPtr StrBlobPtr::operator++(int){
   StrBlobPtr ret = *this; //记录当前的值
   ++*this;   //向前移动一个元素 同时前置++检查递增的有效性
   return ret;   //返回原始状态
}

//后置递减版本
StrBlobPtr StrBlobPtr::operator--(int){
   StrBlobPtr ret = *this; //记录当前的值
   --*this;   //向前移动一个元素 同时前置--检查递增的有效性
   return ret;   //返回原始状态
}

显式调用后置运算符

StrBlobPtr p(a1);  //p指向a1中的vector
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];
        //*p是vector对象
    }
   std::string* operator->() const{
   //把实际工作给了解引用运算符
       return & this->operator*();
   }
   //其他成员
};

值得注意的是,我们将这两个运算符定义成了const成员,这是因为与递增递减运算符不一样,获取一个元素并不会改变StrBlobPtr 对象的状态,同时,他们返回的分别是非常量string的引用或者指针,因为一个StrBlobPtr只能够绑定到非常量的StrBlob对象

StrBlob a1 = {"hi", "bye", "now"};
StrBlobPtr p(a1);  //p指向a1中的vector
*p = "okey";       //给a1的首元素赋值
cout << p->size() <<endl;   //打印4,这是a1首元素的搭小
cout << (*p).size() <<endl;  //等价于p->size()

对箭头运算符返回值的限定
和大多数运算符一样(尽管并不太好),我们令operator完成任何我们指定的操作,换句话说,我们可以让operator返回一个固定值42,或者打印对象的内容或者其他。但是箭头运算符则不是这样,他永远不能丢掉成员访问的基本含义,当我们重载箭头时,可以改变的是箭头从哪个对象当中获取成员,而箭头获取成员这一个事实永远不变。
对于point->mem这样类似的表达式,point必须是指向类对象的指针或者一个重载了operator->的类的对象,等价于

(*point).mem;   //point是一个内置的指针类型
point.operator()->mem;   //point是类的一个对象

注意:重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。

八、14.8 函数调用运算符

如果类重载了函数调用运算符,那么我们可以像使用函数那样实用类。

struct absInt{
    int operator()(int val) const{
        return val < 0 ? -val : val;
    }
};

这个类只定义了一种操作:函数调用运算符,它负责接受一个int实参,然后返回其绝对值。

//调用
int i = -42;
absInt absObj;
int ui = absObj(i);   //将i传递给absObj.operator()

absObj只是一个对象,我们也能 “调用” 此对象
注意:函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或者类型上有所区别;
如果类定义了调用运算符,则该类的对象称为函数对象,即可以调用这种对象,所以我们说这些对象“行为像函数一样”
含有状态的函数对象类

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 printer;   //使用默认值,打印到const
printer(s);            //在cout中打印s,后面跟一个空格
PrintString errors(cerr, '\n');
erros(s);              //在cerr中打印s,后面跟一个换行符

函数对象可以作为泛型算法的实参,例如使用for_each:

for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));

第三个参数是类型PrintString的一个临时对象,其中我们用cerr和换行符初始化了该对象,当程序调用for_each时,将会把vs中的每一个元素依次打印到cerr中,元素之间以换行符分隔。

14.8.1 lambda是函数对象

当我们写了一个lambda之后,编译器将该表达式翻译成一个未命名类的未命名对象,在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 a.size() < b.size();
    }
};
stable_sort(words.begin(), words.end(), ShorterString());

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

//获得第一个指向满足条件元素的迭代器,该元素满足size() >= sz
auto wc = find_if(words.begin(), words.end(),
                   [sz](const string &a)
                      { return a.size() >= sz;});
  //类似于以下行为
class SizeComp{
    SizeComp(size_t n): sz(n){ }  //该形参对应捕获的变量
    //该调用运算符的返回类型、形参、函数体都与lambda一致
    bool operator()(const string &s) const{
       return s.size() >= sz;
    }
private:
   size_t sz;
};
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));

14.8.2 标准库定义的函数对象

标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义一个执行命名操作的调用运算符,例如:plus类定义了一个函数调用运算符用于一对运算对象执行+操作,modulus类定义了一个调用运算符执行二元的%操作,equal_to类执行==,等等。
这些类都被定义成了模板,我们可以为其指定具体的应用类型

plus<int> intAdd;       //执行加法的函数对
negate<int> intNegate;  //可对int值取反的函数对象
//使用intAdd::operator(int, int)求和
int sum = intAdd(10, 20);   //等价于sum = 30;
sum = intNegate(intAdd(10, 20));  //等价于sum=-30;

在这里插入图片描述
在算法中使用标准库函数对象

sort(svec.begin(), svec.end(), greater<string>());

按照上面的语句将按照降序对svec进行排序,第三个实参是greater<string>类型的一个未命名对象,因此当sort比较元素时,不再使用默认的<,而是调用给定的greater 函数对象,该对象负责在string元素之间执行>操作。

14.8.3 可调用对象与function

C++语言有几种可调用对象:函数,函数指针,lambda表达式、bind创建的对象,以及重载了函数调用运算符的类;和其它对象一样,可调用的对象也有类型,每个lambda有他唯一的类类型,函数以及函数指针的类型则由返回值类型和实参类型决定。
然而,不同的类型的可调用对象却可能共享一种调用形式

// int (int, int)
//1.普通函数
int add(int i, int j){ return i+j; }
//2.lambda表达式
auto mod = [](int i, int j){ return i % j; }
//3.函数对象类
struct divide{
   int operator()(int denominator, int divisor){
       return denominator / divisor;
   }
};

上述可调用对象分别对其参数执行不同的算术运算,尽管它们的类型各不相同,但是同享一种调用形式int(int, int);
我们可能希望使用这些可调用对象构建一个简单的桌面计算器,为了实现这一目的,我们需要定义一个函数表,用于存储指向这些可调用对象的“指针”;
标准库function 类型
我们可以使用一个名字为 function 的新的标准库类型解决上述问题,funciton定义在functional 头文件中;

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
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});    //产生歧义,添加哪个add?

//解决方法之一:存储函数指针
int (*fp)(int, int) = add;
binops.insert({"+", fp});   //正确  指向一个正确的add版本

//解决方法之一:使用lambda
binops.insert({"+", [](int a, int b){ return add(a, b);}})

lambda内部的函数调用传入了两个int,因此调用只能接受两个int的add版本,这也正是执行lamda时真正调用的函数。

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

转换构造函数和类型转换运算符共同定义了类类型转换,这样的转换有时也被称作用户定义的类型转换

14.9.1 类型转换运算符

类型转换运算符(conversion operator)是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型,表示一般为:operator type() const;
其中type表示某种类型。类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该类型能作为函数的返回类型,因此,我们不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针以及函数指针)或者引用类型。
定义含有转换运算符的类

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隐式转换成SmaalInt 然后调用SmallInt::operator=
si + 3;     //首先将si隐式转换为int,然后执行整数的加法

我们可以将任何算术类型传递给SmallInt的构造函数,类似的,我们也能使用类型转换运算符将一个SmallInt对象转换为int,然后再将int转换为任何其他类型

//内置转换类型将double转换为int
SmallInt si = 3.14;   //调用SmallInt(int) 构造函数
//SmallInt类型转换运算符将si转换为int
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不是一个指针
};

显示的类型转换运算
c++11引入了显示的类型转换运算符

class SmallInt{
public:   
    //编译器不会自动执行
    explicit operator int() const { return val;}
    //其他成员
};
SmallInt si = 3;     //正确:SmallInt构造函数不是显式的
si + 3;              //错误:此处需要隐式的类型转换,但是类的运算符是显式的
static_cast<int>(si) + 3;    //正确:显示请求类型转换

转换为bool
无论我们在什么时候使用流对象,对会使用为IO类型定义的operator bool

while(std::cin >> value)

while语句的条件执行输入运算符,它负责将数据读入到value,并返回cin,为了对条件求值,cin被istream operator bool类型转换函数隐式执行了转换,如果cin状态是good,则函数返回为真,否则返回假。
注意:向bool类型转换通常用在条件部分,因此operator bool一般定义成explicit

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

如果类中包含一个或多个类型转换,则必须保证在类类型之间只存在唯一一种转换方式,否则的话,我们写的代码会产生二义性。
实参匹配和相同的类型转换

//最好不要在两个类之间构建相同的类型转换
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&))         

同时存在两种由B获得A的方法,所以造成编译器无法判断应该运行哪个类型转换。该调用可以使用以B为参数的A的构造函数,也可以使用B当中把B换成A的类型转换运算符

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 f2(long double);
A a;
f2 (a);   //二义性错误 f(A::operator int())  or   f(A::operator double())
long lg;
A a2(lg);   //二义性错误 A::A(int)    or    A::A(double)

在上面的两个类型转换中哪个都不比另一个好,调用产生二义性;之所以会产生二义性,是因为他们所需的标准类型转换级别一致,如果我们使用用户定义的类型转换时,如果转换过程包含标准类型转换,则标准类型转换的级别将决定编译器选择最佳匹配的过程

short s = 42;
A a3(s);   //正确 使用A::A(int)  把short提升成int要优先于把short转换为double

重载函数和转换构造函数

struct C{
    C(int);
    //其他成员
};
struct D{
   D(int);
   //其他成员
};
void manip(const C&);
void manip(const D&);
manip(10);  //二义性错误 manip(C(10))  or  manip(D(10))

14.9.3 函数匹配与重载运算符

重载的运算符也算是重载的函数。因此,通用的函数匹配规则同样适用于判断在给定的表达式中到底应该使用内置运算符还是重载运算符。

a.operatorsym(b);  //a有一个operatorsym成员函数
operatorsym(a,b);  //opertorsym是一个普通函数

注意:表达式中的运算符的候选函数集既包括成员函数,也包括非成员函数。

class SmallInt{
   friend
   SmallInt operator+(const SmallInt&, const SmallInt&);
public:
   SmallInt(int = 0);                    //转换源为int的类型转换
   operator int() const { return val; }  //转换目标为int的类型转换
private:
   std::size_t val;
};

SmallInt s1, s2;
SmallInt s3 = s1 + s2;     //使用重载的函数
int i = s3 + 0;    //二义性错误

第一个加法我们可以接受两个SmallInt值并执行+运算符的重载版本,第二个加法具有二义性:因为我们可以把0转换为SmallInt,然后执行SmallInt类型加法,也可以把s3转换为int,然后对于两个int执行内置的加法操作。
注意:如果我们对于同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则会遇到上述的二义性问题!

总结

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值