《C++ Primer》第14章 重载运算与类型转换(二)

发布时间:2024年01月11日

参考资料:

  • 《C++ Primer》第5版
  • 《C++ Primer 习题集》第5版

14.8 函数调用运算符(P506)

如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。这样的类同时也能存储状态,所以它们比普通函数更加灵活。我们先考虑这样一个简单的类:

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

int i = -42;
absInt absObj;
int ui = absObj(i);    // ui的值为42

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

如果类定义了调用运算符,则该类的对象称作函数对象(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;    // 分隔符
};

PrintString printer;
printer(s);     //在cout中打印s,后面跟一个空格
PrintString errors(cerr, '\n');
errors(s);     // 在cerr中打印s,后面跟一个换行符
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));

14.8.1 lambda是函数对象(P507)

当我们编写一个 lambda 后,编译器将该表达式翻译成一个未命名类未命名对象,该类中有一个重载的函数调用运算符。例如,我们之前传递给 stable_sortlambda 表达式:

stable_sort(words.begin(), words.end(),
           [](const string &a, const string &b)
            {return a.size()<b.size();});

其行为类似于下面这个类的未命名对象:

class ShorterString {
public:
	bool operator()(const string &a, const string &b) const {
		return a.size() < b.size();
	}
};

lambda 产生的类有一个函数调用运算符成员,其返回值类型、形参列表、函数体均与 lambda 表达式完全一样。默认情况下 lambda 不能改变它捕获的变量,因此 lambda 产生的类中的函数调用运算符是一个 const 成员函数,除非 lambda 被声明成可变的。

我们用上面的类替代 lambda 表达式:

stable_sort(words.begin(), words.end(), ShorterString());    // 创建一个ShorterString对象

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

前面提到过,如果 lambda 引用捕获变量时,程序负责保证 lambda 执行时所引用的对象确实存在,所以编译器可以直接使用该引用无需lambda 类中将其存储为数据成员

相反,通过值捕获的变量将被拷贝到 lambda 中。因此,此种 lambda 产生的类必须为每个值捕获的变量建立数据成员,同时创建构造函数。例如我们有这样一个 lambda

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

它产生的类形如:

class SizeComp{
public:
    SizeComp(size_t n): sz(n) { }    // 值捕获对应变量
    bool operator()(const string &s) const    // 返回类型、形参、函数体均与lambda一致
    	{return s.size() >= sz; }
private:
    size_t sz;
}

14.8.2 标准库定义的函数对象(P509)

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

560794de0b9e271cdb847627af39899
plus<int> intAdd;
negate<int> intNegate;
int sum = intAdd(10, 20);
sum = intNegate(intAdd(10, 20));

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

如果想要执行降序排列,我们可以向 sort 传入一个 greater 类型的对象:

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

我们之前提到过,比较两个无关的指针是未定义的行为。但有时候,我们希望根据内存地址对指针 vector 进行排序,我们可以使用 less 类型的对象:

vector<string*> nameTable;
sort(nameTable.begin(), nameTable.end(),
     [](string *a, string *b) {return a < b;});    // 错误,<是未定义行为
sort(nameTable.begin(), nameTable.end(), less<string*>());    // 正确

14.8.3 可调用对象与function(P511)

C++ 中有集中可调用的对象:函数、函数指针、lambda 表达式、bind 创建的对象、重载了调用运算符的类。可调用对象也有类型,每个 lambda 有自己独有的类型,函数和函数指针的类型由返回值类型参数类型决定。

然而,两个不同类型的调用对象,却可能共享同一种调用形式(call signature)。调用形式指明了调用的返回类型及调用所需的参数类型,一种调用形式对应一种函数类型

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) 。我们可能想使用上述可调用对象构建一个简单的计算器。为了达成这一目的,我们需要定义一个函数表,用于存储这些可调用对象的指针。函数表可以通过 map 实现,将表示运算符号的 string 作为关键字。我们会遇到这样的问题:

map<string, int(*)(int, int)> binops;
binops.insert({"+", add});    // 正确
binops.insert({"%", mod});    // 错误,mod不是一个函数指针
                              //(然而vs2022和devc++都不报错,且能正常使用)
binops.insert({".", divide});    // 错误,原因同上

标准库function类型

头文件 functional 头文件中定义了名为 function 新标准库类型,可以解决上面提到的问题:

a598a10895741947b4d8fcd2d49b31e

function 是一个模板,在创建一个具体的 function 类型时需要对象的调用形式:

function<int(int, int)> f1 = add;
function<int(int, int)> f2 = divide();
function<int(int, int)> f3 = mod;

使用 function ,我们可以重新定义 map

map<string, function<int(int, int)>> binops = {
	{"+", add},
	{"-", std::minus<int>()},
	{"/", divide()},
	{"*", [](int i, int j) {return i * j; }},
	{"%", mod}
};

重载的函数与function

直接将重载函数的名字存入 funtion 类型的对象中,将产生二义性错误

int add(int i, int j) { return i + j; }
double add(double i, double j) { return i + j; }
map<string, function<int(int, int)>> binops({ "+", add });    // 错误,不知道是哪个add

解决上述二义性问题的一条途径是存储函数指针:

int (*fp)(int, int) = add;
binops.insert({ "+", add });

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

转换构造函数类型转换运算符共同定义了类类型转换(class-type conversions)

14.9.1 类型转换运算符(P514)

类型转换运算符(conversion operator)是类的一种特殊成员函数,负责将一个类类型的值转换为其他类型:

operator type() const;

其中,type 可以是除 void 外的任意一种类型,只要该类型能作为函数的返回类型。类型转换运算符没有显式的返回类型,也没有形参,而且必须定义成类的成员函数。类型转换运算符不应改变转换对象的内容,因此类型转换运算符一般被定义成 const 成员

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

我们定义一个简单的类,用来表示 0~255 之间的一个整数:

class SmallInt {
public:
	SmallInt(int i = 0) :val(i) {
		if (i<0 || i>255) {
			throw out_of_range("Bad SmallInt value");
		}
	}
	operator int() const { return val; }
private:
	unsigned val;
};

SmallInt 类既定义了向类类型的转换,也定义了从类类型向替他类型的转换:

SmallInt si;
si = 4;
si + 3;    // si被隐式转换成int,然后执行整型加法

编译器一次只能执行一个用户定义的类型转换,但隐式的用户定义类型转换可以置于标准类型转换之前或之后,并与其一起使用。因此,我们可以将任何算术类型传递给 SmallInt 的构造函数,也能将 SmallInt 对象转换成 int ,再将 int 转换成任何算术类型对象:

SmallInt si = 3.14;
si + 3.14;

由于类型转换运算符是隐式执行的,所以无法给其传递参数,当然也就不能再类型转换运算符的定义中使用任何形参。

避免过度使用类型转换函数,如果类类型和转换的目标类型之间不存在明显的映射关系,则这样的类型转换可能存在误导性。

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

在实践中,类很少提供类型转换运算符,一个例外情况是,人们常常会定义向 bool 类型的转换。

然而,如果一个类想定义一个向 bool 的类型转换,常常会遇到一个问题:由于 bool 是一种算术类型,所以类类型转换成 bool 可能被转换成其他算术类型,从而引发意想不到的结果。

显式的类型转换说明符

为了防止上述异常状况的发生,C++11 新标准引入了显式的类型转换运算符(explicit conversion operator)

class SmallInt {
public:
	explicit operator int() const { return val; }
};

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

编译器不会将一个显式的类型转换运算符用于隐式类型转换,唯一的例外是,如果表达式被用作条件,显式的类型转换(必须有转换成 bool 的运算符)将被隐式执行

转换为bool

bool 的类型通常用在条件部分,operator bool 一般被定义成 explicit

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

如果类中包含类型转换,必须确保类类型和目标类型之间只存在唯一一种转换方式。两种情况可能产生多重转换路径:

  1. 两个类提供了相同的类型转换:类 A 定义了接受一个 类 B 对象的转换构造函数,类 B 又定义了一个转换目标是类 A 的类型转换运算符
  2. 类定义了多个转换规则,而这些转换涉及的类型又可以通过其他类型转换联系在一起。最典型的例子是算术类型,一个类最好只定义一个与算术类型有关的转换

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

struct B;
struct A {
	A() = default;
	A(const B &);
};
struct B {
	operator A() const;
};
A f(const A &);
B b;
A a = f(b);    // 错误,是f(B::operator A()),还是f(A::A(const B&))?

如果我们一定要执行上述函数调用,我们可以显式调用转换构造函数或类型转换运算符:

A a = f(b.operator A());
A a = f(A(b));

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

struct A {
	A(int = 0) { cout << "int -> A"; }
	A(double) {cout << "double->A"; }
	operator int() const { cout << "A -> int"; return val; };
	operator double() const { cout << "A -> double"; return val; }
	int val;
};
void f(long double);

A a;
f(a);    // 错误,是f(a.operator int()),还是f(a.operator double())?
long lg;
A a2(lg);    // 错误,是A(int),还是A(double)?
short s = 0;
A a3(s);    // 正确,short被提升成int,然后执行A(int)

除了显式地向 bool 类型的转换之外,我们应尽量避免定义转换目标是内置类型的转换。

重载函数与转换构造函数

struct C {
	C(int);
};
struct D {
	D(int);
};
void manip(const C &);
void manip(const D &);

manip(10);    // 错误,是manip(C(10)),还是manip(D(10))?

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

当调用重载函数时,如果多个用户定义的类型转换都提供了可行匹配,则编译器认为这些类型转换一样好,且编译器不会考虑任何可能出现的标准类型转换的级别

struct C {
	C(int);
};
struct E {
	E(double);
};
void manip(const C &);
void manip(const E &);

manip(10);    // 错误,是manip(C(10)),还是manip(E(double(10)))?

只有当重载函数能通过同一个类型转换得到匹配时,编译器才会考虑其中出现的标准类型转换:

struct F{
	operator int();  
};
void manip(int);
void manip(double);

F f;
manip(f);    // 正确,调用manip(int)

14.9.3 函数匹配与重载运算符(P521)

重载的运算符也是重载的函数,当运算符函数出现在表达式中时,候选函数集的范围可能比调用普通重载函数更大。如果运算符的左侧运算对象是类类型,则候选函数将同时包含内置运算符,以及该运算符的非成员版本和成员版本。而对于一般的函数调用,即使成员函数和非成员函数重名,但它们的调用方式是不一样的。

class SmallInt {
public:
	SmallInt(int i = 0) :val(i) { }
	SmallInt operator+(const SmallInt &a) {
		cout << "member plus" << endl;
		return SmallInt(a.val + val);
	}
	friend SmallInt operator+(const SmallInt &, const SmallInt &);
	operator int() { return val; }
private:
	unsigned val;
};

1 + s1;    // "common plus"
s1 + 1;    // "member plus",居然没有二义性错误吗?
s1 + s2;    // "member plus"

如果在上面的 SmallInt 中加入 operator int() { return val; } 成员,那么 1 + s1s1 + 1 将出现二义性错误,因为无法确定调用重载 + 还是标准 +

文章来源:https://blog.csdn.net/MaTF_/article/details/135521954
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。