14.重载运算与类型转换

news2024/9/29 3:21:40

文章目录

  • 重载运算与类型转换
    • 14.1基本概念
      • 直接调用一个重载的运算符函数
      • 某些运算符不应该被重载
      • 使用与内置类型一致的含义
      • 赋值和复合赋值运算符
      • 选择作为成员或者非成员
    • 14.2输入和输出运算符
      • 14.2.1重载输出运算符<<
        • 输出运算符尽量减少格式化操作
        • 输入输出运算符必须是非成员函数
      • 14.2.2重载输入运算符>>
        • 标示错误
    • 14.3算术和关系运算符
      • 14.3.1相等运算符
      • 14.3.2关系运算符
    • 14.4赋值运算符
      • 复合赋值运算符
    • 14.5下标运算符
    • 14.6递增和递减运算符
      • 定义前置递增/递减运算符
      • 区分前置和后置运算符
      • 显式地调用后置运算符
    • 14.7成员访问运算符
      • 对箭头运算符返回值的限定
    • 14.8函数调用运算符
      • 含有状态的函数对象类
      • 14.8.1Lambda是函数对象
        • 表示lambda及相应捕获行为的类
      • 14.8.2标准库定义的函数对象
        • 在算法中使用标准库函数对象
      • 14.8.3可调用对象与function
        • 不同类型可能具有相同的调用形式
        • 标准库function类型
        • 重载的函数与function
    • 14.9重载、类型转换与运算符
      • 14.9.1类型转换运算符
        • 定义含有类型转换运算符的类
        • 类型转换运算符可能产生意外结果
        • 显式的类型转换运算符
      • 14.9.2避免有二义性的类型转换
        • 实参匹配和相同的类型转换
        • 二义性与转换目标为内置类型的多重类型转换
        • 重载函数与转换构造函数
        • 重载函数与用户定义的类型转换
      • 14.9.3函数匹配与重载运算符

重载运算与类型转换

14.1基本概念

重载的运算符由关键字operator和其后要定义的运算符号共同组成,同时包含返回类型、参数列表以及函数体。其参数数量与该运算符作用的运算对象数量一样多。例如,一元运算符有一个参数,二元运算符有两个。除了重载的函数调用运算符operator()之外,其他重载运算符不能包含有默认实参
当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的(显式)参数数量比运算对象的数量少一个。

在这里插入图片描述

对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。

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

// 假设data1和data2是同一个类的不同实例
// 一个非成员运算符函数的等价调用
data1 + data2;	// 普通的表达式
operator+(data1, data2);	// 等价的函数调用
// 将this绑定到data1的地址,将data2作为实参传入了函数。
data1 += data2;	// 基于调用的表达式
data1.operator+=(data2);	// 对成员运算符函数的等价调用

某些运算符不应该被重载

某些运算符指定了运算对象求值的顺序,因为使用重载的运算符本质上是一次函数调用,所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上。例如,&&||、‘,’(逗号运算符)。除此之外,&&||的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。
另一方面,c++已经定义了,&用于类类型对象时的特殊含义,所以一般来说不应该被重载,否则其行为将异于常态,从而导致类的用户无法适应。

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

如果某些操作在逻辑上与运算符相关,则它们适合于定义成重载的运算符:

  • 如果类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致。
  • 如果类的某个操作是检查相等性,则定义operator==,此时,通常也应该有operator!=
  • 如果类包含一个内在的单序比较操作,则定义operator<,此时,通常也应该有其他关系操作。
  • 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容:逻辑运算符和关系运算符应该返回bool,算术运算符应该返回一个类类型的值,赋值运算符和复合赋值运算符则应该返回左侧运算对象的一个引用。

赋值和复合赋值运算符

如果类含有算术运算符或者位运算符,则最好也提供对应的复合赋值运算符

选择作为成员或者非成员

  • 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
  • 复合赋值运算符一般来说应该是成员,但并非必须。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员。
  • 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。

当把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象:

string s = "world";
// 正确:能把一个const char*加到一个string对象中。
string t = s + "!";
// 如果+是string的成员,则产生错误。其等价于:"hi".operator+(s),然而,由于"hi"的类型
// 是const char*,这是一种内置类型,根本就没有成员函数。因为string把+定义成了普通的非成员
// 函数,所以其等价于operator+("hi", s)。唯一的要求是至少有一个运算对象时类类型。
string u = "hi" + s;

14.2输入和输出运算符

14.2.1重载输出运算符<<

通常情况下,输出运算符的第一个形参是非常量ostream对象的引用。第二个形参一般来说是一个常量的引用,该常量是想要打印的类类型。
为了与其他输出运算符保持一致,operator<<一般要返回它的ostream形参。

ostream &operator<<(ostream &os, const Sales_data &item) {
    os << item.isbn() << " " << item.units_sold << ""
       << item.revenue << " " << item.avg_price();
    return os;
}

输出运算符尽量减少格式化操作

通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。

输入输出运算符必须是非成员函数

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

假设输入输出运算符是某个类的成员,则它们也必须是istreamostream的成员。然而,这两个类属于标准库,并且无法给标准库中的类添加任何成员。
另一方面,IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元

14.2.2重载输入运算符>>

通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量)对象的引用。该运算符通常会返回某个给定流的引用。

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

输入运算符必须处理输入可能失败的情况,而输出运算符不需要。

标示错误

一些输入运算符需要做更多数据验证的工作。例如,检查是否符合规范的格式。在这样的例子中,即使从技术上来看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相等运算符

如果某个类在逻辑上有相等性的含义,则该类应该定义operator==,这样做可以使得用户更容易使用标准库算法来处理这个类。

bool operator==(const Sales_data &lhs, const Sales_data &rhs) {
	return lhs.isbn() == rhs.isbn()
		&& lhs.unit_sold = rhs.units_sold
		&& lhs.revenue = rhs.revenue;
}

bool operator!=(const Sales_data &lhs, const Sales_data &rhs) {
	return !(lhs == rhs);
}

14.3.2关系运算符

定义了相等运算符的类也常常(但不总是)包含关系运算符。特别是,因为关联容器和一些算法要用到小于运算符,所以定义operator<会比较有用。
通常情况下,如果两个对象是!=的,那么一个对象应该<另外一个。

14.4赋值运算符

拷贝赋值和移动赋值运算符可以把类的一个对象赋值给该类的另一个对象。此外,类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象。例如,vector的花括号列表赋值:

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

同样,也可以把这个运算符添加到StrVec类中(参考13.5节):

class StrVec {
public:
	StrVec &operator=(std::initializer_list<std::string>);
	// 其他成员与前面一致
};

// 无须检查自赋值,形参initializer_list<std::string>确保il与this所指的不是同一个对象。
StrVec &StrVec::operator=(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;
}

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;	// 指向数组首元素的指针
};

14.6递增和递减运算符

定义递增和递减运算符的类应该同时定义前置版本和后置版本。因为它们改变的正好是所操作对象的状态,所以这些运算符通常应该被定义成类的成员。

定义前置递增/递减运算符

为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。

// 参考12.1.6节
class StrBlobPtr {
public:
	// 递增和递减运算符
	StrBlobPtr &operator++();	// 前置运算符
	StrBlobPtr &operator--();
	// 其他成员保持一致
};

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

StrBlobPtr &StrBlobPtr::operator--() {
	// 如果curr是0,则继续递减它将产生一个无效下标。
	--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.operator++(0);	// 调用后置版本的operator++
p.operator++();	// 调用前置版本的operator++

14.7成员访问运算符

在迭代器类及智能指针类中常常用到解引用运算符(*)和箭头运算符(->)。

class StrBlobStr {
public:
	std::string &operator*() const {
		auto p = check(curr, "dereference past end");
		return (*p)[curr];	// (*p)是对象所指的vector
	}

	// 这里返回指针需要结合后面的知识来理解
	std::string *operator->() const {
		// 将实际工作委托给解引用运算符
		return &this->operator*();
	}
	// 其他成员保持一致
};

对箭头运算符返回值的限定

箭头运算符永远不能丢掉成员访问这个最基本的含义。当重载箭头时,可以改变的是箭头从哪个对象当中获取成员,而箭头获取成员这一事实则永远不变。
对于形如point->mem的表达式来说,point必须是指向类对象的指针或者是一个重载了operator->的类的对象。根据point类型的不同,point->mem分别等价于:

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

除此之外,代码都将发生错误。point->mem的执行过程如下:

  1. 如果point是指针,则应用内置的箭头运算符,表达式等价于(*point).mem。如果point所指的类型没有名为mem的成员,程序会发生错误。
  2. 如果point是定义了operator->的类的一个对象,则使用point.operator->()的结果来获取mem。其中,如果结果是一个指针,则执行第1步;如果该结果本身含有重载的operator->,则重复调用当前步骤。最终,当这一过程结束时程序或者返回了所需的内容,或者返回一些表示程序错误的信息。

14.8函数调用运算符

如果类重载了函数调用运算符,则可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通函数相比更加灵活。

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

int i = -42;
absInt absObj;	// 含有函数调用运算符的对象
int ui = absObj(i);	// 将i传递给absObj.operator()

函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。

含有状态的函数对象类

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

函数对象常常作为泛型算法的实参

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

14.8.1Lambda是函数对象

当编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。在lambda表达式产生的类中含有一个重载的函数调用运算符:

// 根据单词的长度对其进行排序,对于长度相同的单词按照字母表顺序排序。
stable_sort(words.begin(), words.end(), 
			[](const string &a, const string &b) { return a.size() < b.size(); });

// 其行为类似于这个类的一个未命名对象:
class ShorterString {
	bool operator()(const string &s1, const string &s2) const {
		return s1.size() < s2.size();
	}
};

stable_sort(words.begin(), words.end(), ShorterString());

默认情况下lambda不能改变它捕获的变量。因此在默认情况下,由lambda产生的类当中的函数调用运算符是一个const成员函数。如果lambda被声明为可变的,则调用运算符就不是const的了。

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

当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时引用所引的对象确实存在。因此,编译器可以直接使用该引用而无须在lambda产生的类中将其存储为数据成员。
相反,通过值捕获的变量被拷贝到lambda中。因此,这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。

auto wc = find_if(words.begin(), words.end(),
                  [sz](const string &a) { return a.size() >= sz; });

// 产生的类形如:
class SizeComp {
public:
    SizeComp(size_t n) : sz(n) {}   // 该形参对应捕获的变量
    // 该调用运算符的返回类型、形参和函数体都与lambda一致。
    bool operator()(const string &s) const {
        return s.size() >= sz;
    }

private:
    size_t sz;  // 该数据成员对应通过值捕获的变量
};

Lambda表达式产生的类不含默认构造函数、赋值运算符及默认析构函数;它是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定。

14.8.2标准库定义的函数对象

标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。

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

在这里插入图片描述

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

表示运算符的函数对象类常用来替换算法中的默认运算符。

// 传入一个临时的函数对象用于执行两个string对象的>比较运算
sort(svec.begin(), svec.end(), greater<string>());

需要特别注意的是,标准库规定其函数对象对于指针同样适用:

// 正常情况下,比较两个无关指针将产生未定义的行为。
vector<string*> nameTable;	// 指针的vector
// 错误:nameTable中的指针彼此之间没有关系,所以<将产生未定义的行为。
sort(nameTable.begin(), nameTable.end(), 
	[](string *a, string *b) { return a < b; });
// 正确:标准库规定指针的less是定义良好的。
sort(nameTable.begin(), nameTable.end(), less<string*>());

关联容器使用less<key_type>对元素排序,因此可以定义一个指针的set或者在map中使用指针作为关键字而无须直接声明less

14.8.3可调用对象与function

C++中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。
和其他对象一样,可调用的对象也有类型。然而,两个不同类型的可调用对象却可能共享同一种调用形式

不同类型可能具有相同的调用形式

// 普通函数
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)

// 可能希望使用这些可调用对象构建一个简单的桌面计算器。为了实现这一目的,
// 需要通过map定义一个函数表:
// 构建从运算符到函数指针的映射关系,其中函数接受两个int,返回一个int。
map<string, int(*)(int, int)> binops;
// 可以将add指针添加到binops中:
binops.insert({"+", add});
// 但是不能将mod或者divide存入binops,问题在于mod是个lambda表达式,而每个
// lambda有它自己的类类型,该类型与存储在binops中的值的类型不匹配。

标准库function类型

可以使用一个名为function的新的标准库类型解决上述问题,定义在functional头文件中。

在这里插入图片描述

function是一个模板,需要提供能够表示的对象的调用形式:

function<int(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; };	// Lambda

cout << f1(4, 2) << endl;	// 6
cout << f2(4, 2) << endl;	// 2
cout << f3(4, 2) << endl;	// 8

// 因此可以重新定义map:
map<string, function<int(int, int)>> binops = {
	{"+", add}, 
	{"-", std::minus<int>()}, 
	{"/", divide()}, 
	{"*", [](int i, int j) { return i * j; }}, 
	{"%", mod}
};

// function类型重载了调用运算符,该运算符接受它自己的实参,然后
// 将其传递给存好的可调用对象:
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); }});

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

14.9.1类型转换运算符

类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。

// type表示某种类型
operator type() const;

类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该类型能作为函数的返回类型。因此,不允许转换成数组或者函数类型,但允许转换成指针或者引用类型。
通常不应该改变待转换对象的内容,因此,类型转换运算符一般被定义成const成员。

定义含有类型转换运算符的类

class SmallInt {
public:
	// 定义了向类类型的转换
	SmallInt(int i = 0) : val(i) {
		if (i < 0 || i > 255) {
			throw std::out_of_range("Bad");
		}
	}

	// 定义从类类型向其他类型的转换
	operator int() const {
		return val;
	}

private:
	std::size_t val;
};

SmallInt si;
si = 4;	// 首先将4隐式地转换成SmallInt,然后调用SmallInt::operator=。
si + 3;	// 首先将si隐式地转换成int,然后执行整数的加法。

尽管编译器一次只能执行一个用户定义的类型转换,但是隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用

// 内置类型转换将double实参转换成int
SmallInt si = 3.14;	// 调用SmallInt(int)构造函数
// SmallInt的类型转换运算符将si转换成int
si + 3.14;	// 内置类型转换将所得的int继续转换成double

明智地使用类型转换运算符

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

在实践中,类很少提供类型转换运算符。在大多数情况下,如果类型转换自动发生,用户可能会感觉比较意外,而不是感觉受到了帮助。然而,定义向bool的类型转换还是比较普遍的现象
在c++标准的早起版本中,如果类想定义一个向bool的类型转换,则会遇到一个问题:因为bool是一种算术类型,所以类类型的对象转换成bool后就能被用在任何需要算术类型的上下文中。

int i = 42;
// istream本身并没有定义<<,所以应该产生错误。然而,该代码能使用istream的bool
// 类型转换运算符将cin转换成bool,而这个bool值接着会被提升成int并用作内置的左移
// 运算符的左侧运算对象。
cin << i;

显式的类型转换运算符

C++11引入了显式的类型转换运算符

class SmallInt {
public:
	// 编译器不会自动执行这一类型转换
	explicit operator int() const {
		return val;
	}
	// 其他成员保持一致
};

编译器(通常)也不会将一个显式的类型转换运算符用于隐式类型转换:

SmallInt si = 3;	// 正确:SmallInt的构造函数不是显式的。
si + 3;	// 错误:此处需要隐式的类型转换,但类的运算符是显式的。
static_cast<int>(si) + 3;	// 正确:显式地请求类型转换。

但是也存在例外,即,如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它。

  • ifwhiledo语句的条件部分。
  • 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;
// 二义性错误:含义是f(B::operator A())还是f(A::A(const B &))?
A a = f(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;
	// 其他成员
};

// 哪个类型转换都无法精确匹配long double
void f2(long double);
A a;
// 二义性错误:含义是f(A::operator int())还是f(A::operator double())?
f2(a);

long lg;
// 二义性错误:含义是A::A(int)还是A::A(double)?
A a2(lg);

当使用用户定义的类型转换时,如果转换过程包含标准类型转换,则标准类型转换的级别将决定编译器选择最佳匹配的过程

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

要想正确地设计类的重载运算符、转换构造函数及类型转换函数,必须加倍小心。尤其是当类同时定义了类型转换运算符及重载运算符时特别容易产生二义性。相关的经验规则:

  • 不要令两个类执行相同的类型转换:如果Foo类有一个接受Bar类对象的构造函数,则不要在Bar类中再定义转换目标是Foo类的类型转换运算符。
  • 避免转换目标是内置算术类型的类型转换。特别是当已经定义了一个转换成算术类型的类型转换时,接下来:
    • 不要再定义接受算术类型的重载运算符。如果用户需要使用这样的运算符,则类型转换操作将转换你的类型的对象,然后使用内置的运算符。
    • 不要定义转换到多种算术类型的类型转换。让标准类型转换完成向其他算术类型转换的工作。

因此,除了显式地向bool类型的转换之外,应该尽量避免定义类型转换函数并尽可能地限制那些"显然正确"的非显式构造函数

重载函数与转换构造函数

当调用重载的函数时,从多个类型转换中进行选择将变得更加复杂。如果两个或多个类型转换都提供了同一种可行匹配,则这些类型转换一样好。
当几个重载函数的参数分属不同的类类型时,如果这些类恰好定义了同样的转换构造函数,则二义性问题将进一步提升:

struct C {
	C(int);
	// 其他成员
};
struct D {
	D(int);
	// 其他成员
};
void manip(const C &);
void manip(const D &);
// 二义性错误:含义是manip(C(10))还是manip(D(10))。
manip(10);
// 可以显式地构造正确的类型从而消除二义性:
manip(C(10));

如果在调用重载函数时需要使用构造函数或者强制类型转换来改变实参的类型,则这通常意味着程序的设计存在不足。

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

当调用重载函数时,如果两个(或多个)用户定义的类型转换都提供了可行匹配,则认为这些类型转换一样好。在这个过程中,不会考虑任何可能出现的标准类型转换的级别。只有当重载函数能通过同一个类型转换函数得到匹配时,才会考虑其中出现的标准类型转换。

struct E {
	E(double);
	// 其他成员
};

void manip2(const C &);
void manip2(const E &);
// 二义性错误:两个不同的用户定义的类型转换都能用在此处。
manip2(10);	// 含义是manip2(C(10))还是manip2(E(double(10)))

14.9.3函数匹配与重载运算符

重载的运算符也是重载的函数。因此,通用的函数匹配规则同样适用于判断在给定的表达式中到底应该使用内置运算符还是重载的运算符。不过当运算符函数出现在表达式中时,候选函数集的规模要比使用调用运算符调用函数时更大。
如果a是一种类类型,则表达式a sym b可能是:

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

和普通函数调用不同,不能通过调用的形式来区分当前调用的是成员函数还是非成员函数。
当使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内。
当调用一个命名的函数时,具有该名字的成员函数和非成员函数不会彼此重载,这是因为用来调用命名函数的语法形式对于成员函数和非成员函数来说是不相同的。当通过类类型的对象(或者该对象的指针及引用)进行函数调用时,只考虑该类的成员函数。而当在表达式中使用重载的运算符时,无法判断正在使用的是成员函数还是非成员函数,因此二者都应该在考虑的范围内。

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对象相加,但如果试图执行混合模式的算术运算,
// 就将遇到二义性的问题:
SmallInt s1, s2;
SmallInt s3 = s1 + s2;	// 使用重载的operator+
int i = s3 + 0;	// 二义性错误

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/186440.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

智能家居之主机---计划筹备

智能家居之主机---计划筹备前言绪言前期构思硬件平台结构平台前言 绪言 感觉有一年多没发过文章了&#xff0c;这一年多太忙了&#xff0c;来到新的公司后要学的太多了&#xff0c;代码风格&#xff0c;架构&#xff0c;操作系统&#xff0c;各种通讯协议&#xff0c;伺服驱动…

解决问题的方法论

概述 解决问题的能力是职场中最重要的能力之一&#xff0c;如何逻辑清晰、效率满满的解决问题&#xff0c;可参考以下4个步骤。 一、准确的界定问题 找出真正的问题。 准确的界定问题&#xff0c;避免被表面现象所迷惑。 《麦肯锡工具》中&#xff0c;给出一个标准的步骤&am…

【数据手册】LM1117L3芯片的使用

1.特征 可调或固定输出1A输出电流低损耗&#xff0c;在1A输出电流时最大电压为1.3V0.04%的线路调节0.2%负载调节100%热极限燃烧快速瞬态响应 2.描述 LM1117系列正可调和固定调节器设计提供1A高电流效率。所有内部电路设计为低至1.3V输入输出差。片内微调将参考电压调整为1% 3…

【微服务】RabbitMQSpringAMQP消息队列

&#x1f6a9;本文已收录至专栏&#xff1a;微服务探索之旅 &#x1f44d;希望您能有所收获 一.初识MQ (1) 引入 微服务间通讯有同步和异步两种方式&#xff1a; 同步通讯&#xff1a;就像打电话&#xff0c;可以立即得到响应&#xff0c;但是你却不能跟多个人同时通话。 异…

leedcode刷题 | 详细注释 | 调用+调试 C++

目录1.两数之和题目C代码2.两数相加题目代码3. 无重复字符的最长子串题目&#xff1a;代码&#xff1a;4. 合并两个有序数组题目&#xff1a;代码&#xff1a;5.寻找两个正序数组的中位数题目&#xff1a;代码&#xff1a;1.两数之和 题目 给定一个整数数组 nums 和一个整数目…

API 网关策略的二三事

作者暴渊&#xff0c;API7.ai 技术工程师&#xff0c;Apache APISIX Committer。 近些年随着云原生和微服务架构的日趋发展&#xff0c;API 网关以流量入口的角色在技术架构中扮演着越来越重要的作用。API 网关主要负责接收所有请求的流量并进行处理转发至上游服务&#xff0c;…

说一说JVM的垃圾回收器

垃圾回收器1.Serial收集器2.parnew收集器3 .parallel Scavenge收集器4.Serial Old5.parallel old收集器6.cms7. G1 收集器串行&#xff1a;指的是垃圾回收器与用户线程交替进行&#xff0c;这意味着在垃圾回收器执行的时候用户线程需要暂停工作 并行&#xff1a;指的是垃圾回收…

网络知识详解之:CA证书制作实战(Nginx数字证书实战)

网络知识详解之&#xff1a;CA证书制作实战 计算机网络相关知识体系详解 网络知识详解之&#xff1a;TCP连接原理详解网络知识详解之&#xff1a;HTTP协议基础网络知识详解之&#xff1a;HTTPS通信原理剖析&#xff08;对称、非对称加密、数字签名、数字证书&#xff09;网络…

Oracle的学习心得和知识总结(九)|Oracle数据库PL/SQL语言条件选择语句之IF和CASE语句技术详解

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《Oracle Database SQL Language Reference》 2、参考书籍&#xff1a;《PostgreSQL中文手册》 3、EDB Postgres Advanced Server User Guid…

[Lua实战]整理Lua中忽略的问题

整理Lua中忽略的问题1.元表metatable和元方法1.1元方法_index可以设置为table1.2.元方法_index可以设置为函数1.3.元方法_index和_newindex实现只读table2.Lua强制GC方法2.1 collectgarbage()3.协程和线程的区别3.1协程coroutine.create()是同步执行,不是并行,只是切了一个上下…

Day874.MySQL索引选择出错问题 -MySQL实战

MySQL索引选择出错问题 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于MySQL索引选择出错问题的内容。 写 SQL 语句的时候&#xff0c;并没有主动指定使用哪个索引。也就是说&#xff0c;使用哪个索引是由 MySQL 来确定的。 不知道有没有碰到过这种情况&#xff0…

Android开发进阶—invoke反射及其原理解析

反射的概念 反射:Refelection,反射是Java的特征之一,允许运行中的Java程序获取自身信息,并可以操作类或者对象的内部属性通过反射,可以在运行时获得程序或者程序中的每一个类型的成员活成成员的信息程序中的对象一般都是在编译时就确定下来,Java反射机制可以动态地创建对象并且…

含有吲哚基团132557-72-3,2,3,3-三甲基-3H-吲哚-5-磺酸

基础产品数据&#xff08;Basic Product Data&#xff09;&#xff1a;CAS号&#xff1a;132557-72-3中文名&#xff1a;2,3,3-三甲基-3H-吲哚-5-磺酸&#xff0c;2,3,3-三甲基-3H-吲哚-6-磺酸钾盐英文名&#xff1a;5-Sulfo-2,3,3-trimethyl indolenine sodium salt&#xff0…

Axure原型图设计工具使用记录

Axure原型图设计工具使用记录 Axure快速入门&#xff08;01&#xff09; - 面板介绍 https://blog.51cto.com/u_15294985/3007677 Axure快速入门&#xff08;02&#xff09; - 入门例子&#xff08;登录案例&#xff09; https://developer.aliyun.com/article/1046689 Axu…

03 python 要点(函数+类)

第8章 函数 8.1 定义函数 函数就是执行特定任务的一段代码, 自定义函数的语法格式, 以英文半角冒号结尾. # def: 8.2 调用函数 在定义好函数后&#xff0c;既可调用函数. 8.2.1 使用位置参数调用函数 在调用函数时传递的实参与定义函数时的形参顺序一致&#xff0c;这…

LoongArch上正常使用`pip install`

原创&#xff1a;你在使用loongarch架构操作系统时&#xff0c;是否遇到pip install 安装失败的情况&#xff1f; 刷到这篇文章&#xff0c;大家可添加评论或者私信我&#xff0c;及时满足大家的需求那么&#xff0c;下面讲一下如何配置loongarch架构的pip 仓库及开发原理如何配…

Java集合(五)LinkedList底层扩容源码分析

LinkedList的全面说明&#xff1a; &#xff08;1&#xff09;LinkedList底层实现了双向链表和双端队列特点 &#xff08;2&#xff09;可以添加任意元素&#xff08;元素可以重复&#xff09;&#xff0c;包括null. (3)线程不安全&#xff0c;没有实现同步 LinkedList的底…

MicroBlaze系列教程(1):AXI_GPIO的使用

文章目录 @[toc]简介常用函数使用示例参考资料工程下载本文是Xilinx MicroBlaze系列教程的第1篇文章。 简介 AXI GPIO是基于AXI-lite总线的一个通用输入输出IP核,可配置为一个或两个通道,每个通道32位,每一位可以通过SDK动态配置成输入或输出方向,支持中断请求,配合中断控…

计算机网络第三章 传输层

本文部分图片&#xff08;PPT截图&#xff09;来自中科大计算机网络top down3.0 目录[TOC]3.1 概述传输层TCP和UDP协议可以在IP协议主机到主机通信的基础上&#xff0c;实现进程到进程之间的通信&#xff08;利用端口号&#xff09;真正实现端到端的通信【通过多路复用于解复用…

b站黑马Vue2后台管理项目笔记——(3)用户列表

说明&#xff1a; 此项目中使用的是本地SQL数据库&#xff0c;Vue2。 其他功能请见本人后续的其他相关文章。 本文内容实现的最终效果如下图&#xff1a; 三.用户列表的开发 目标效果&#xff1a; 点击二级菜单——用户列表&#xff0c;在右侧展示用户列表对应的内容&#xf…