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

参考资料:

  • 《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 将出现二义性错误,因为无法确定调用重载 + 还是标准 +

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android可换行的RadioGroup

Android可换行的RadioGroup,有时候需要换行显示的单选列表&#xff0c;当然可以有多种实现方式&#xff0c;比如recycleview或者listview实现&#xff0c;本文采用的是RadioGrouprediobutton方式实现。 一、首先自定义view public class WrapRadioGroup extends RadioGroup {pr…

【XR806开发板试用】+ FreeRtos开发环境搭建

获取SDK SDK可以通过官网直接下载。 下载完成之后&#xff0c;通过gzip命令解压文件 gzip -d xr806_sdk.tar.gz 获取编译链工具 还是按照官网操作指南&#xff0c;下载 gcc-arm-none-eabi-8-2019-q3-update 下载之后进行解压&#xff0c;同理。 注意修改GCC路径&#xff0c…

三、C语言分支与循环知识点补充——随机数生成

本章分支结构的学习内容如下&#xff1a; 三、C语言中的分支与循环—if语句 (1) 三、C语言中的分支与循环—关系操作符 (2) 三、C语言中的分支与循环—条件操作符 与逻辑操作符(3) 三、C语言中的分支与循环—switch语句&#xff08;4&#xff09;分支结构 完 本章循环结构的…

直播预告丨看零售场,如何玩转 MaaS

今年&#xff0c;有一个被频繁提及的词是MaaS 这类工具正在帮助千行百业实现大模型落地产业 在零售场&#xff0c;特别是像京东这样拥有超高并发、超复杂协同的电商场内 也沉淀出了一套通用的AI基础设施——九数算法中台 从提升客户服务体验、平台效率出发&#xff0c;训练各…

AtCoder ABC194

这期比193稍微简单一点 C - Squared Error 手玩一下&#xff1a; N 3 N3 N3时 展开得 a 2 b 2 − 2 a b b 2 − c 2 − 2 b c a 2 c 2 − 2 a c a^2b^2-2abb^2-c^2-2bca^2c^2-2ac a2b2−2abb2−c2−2bca2c2−2ac 每个数平方项都要计算 n − 1 n-1 n−1次 减的那一份可…

MYSQL篇--事务机制高频面试题

事务 1 什么是数据库事务&#xff1f; 事务是一个不可分割的数据库操作序列&#xff0c;也是数据库并发控制的基本单位&#xff0c;其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作&#xff0c;要么都执行&#xff0c;要么都不执行。…

图纸版本管理混乱怎么办?彩虹PDM系统帮你搞定!

在现代制造业和工程领域&#xff0c;图纸版本管理的混乱常常是一个棘手的问题。不同版本的图纸可能导致严重的错误和生产问题&#xff0c;影响了产品质量和交付时间。然而&#xff0c;有一个强大的工具可以帮助企业解决这个问题&#xff0c;那就是PDM产品数据管理系统。彩虹PDM…

云流量回溯的工作原理及关键功能

云计算和网络技术的快速发展为企业提供了更灵活、高效的业务运营环境&#xff0c;同时也引发了一系列网络安全挑战。在这个背景下&#xff0c;云流量回溯成为网络安全领域的一个关键技术&#xff0c;为企业提供了对网络活动的深入洞察和实时响应的能力。 一、 云流量回溯的基本…

pkuseg按照用户自定义词典分词错误修正

import pkusegc pkuseg.pkuseg(user_dict"./data/dict.txt") sentence 数字传播实验班 print(c.cut(sentence))字典中包含“”数字传媒与人文学院"&#xff0c;添加自定义词典后&#xff0c;文本被错误分成““数字传 播 实验班” &#xff0c;debug发现solve…

OpenShift 4 - 在 OpenShift 上运行物体检测 AI 应用

《OpenShift / RHEL / DevSecOps 汇总目录》 说明&#xff1a;本文已经在 OpenShift 4.14 RHODS 2.5.0 的环境中验证 说明&#xff1a;请先根据《OpenShift 4 - 部署 OpenShift AI 环境&#xff0c;运行 AI/ML 应用&#xff08;视频&#xff09;》一文完成 OpenShift AI 环境…

python爬虫实战(8)--获取虎pu热榜

1. 需要的类库 import requests from bs4 import BeautifulSoup import pandas as pd2. 请求地址 def fetch_data():url "https://bbs.xxx.com/" # Replace with the actual base URLresponse requests.get(url)if response.status_code 200:return response.c…

2024年最火爆的本地生活服务商平台推荐!

随着互联网的发展&#xff0c;本地生活团购服务市场逐渐成为各大平台争夺的焦点。视频号、DY等短视频平台纷纷入局&#xff0c;希望通过本地生活团购的形式吸引更多的用户&#xff0c;提高平台的活跃度和黏性。对于想要成为本地生活团购服务商的创业者来说&#xff0c;都不想放…

web期末作业网页设计——JavaScript

目录 一.作品简介 二.网页效果 首页 花语 登录界面 注册界面 三.网页代码 首页 登录界面 注册界面 视频界面 一.作品简介 网站系统文件种类包含&#xff1a;html网页结构文件、css网页样式文件、js网页特效文件、images网页图片文件。 网页作品代码简单&#xff…

2024 年 10 款最佳 Android 手机数据恢复软件榜单

当您因某些不合时宜的事故而丢失 Android 设备上的重要数据时&#xff0c;这真是一场灾难。如果您遇到这样的情况&#xff0c;请不要担心。我们列出了一些最好的 Android 数据恢复软件&#xff0c;可以帮助您使用 PC 检索手机丢失的数据。 在用于存储重要数据的各种存储设备中…

你真的掌握了“C语言分支循环”吗

目录 前言 1. if语句 1.1 if 1.2 else 1.3 分支中包含多条语句 1.4 嵌套if 1.5 悬空else问题 2. 关系操作符 3. 条件操作符 4. 逻辑操作符&#xff1a;&& , || , &#xff01; 4.1 逻辑取反运算符 4.2 与运算符 4.3 或运算符 4.4 练习&#xff1a;闰年的判…

【一周年创作总结】人生是远方的无尽旷野呀

那一眼瞥见的伟大的灵魂&#xff0c;却似模糊的你和我 文章目录 &#x1f4d2;各个阶段的experience&#x1f50e;大一寒假&#x1f50e;大一下学期&#x1f50e;大一暑假&#x1f50e;大二上学期&#xff08;现在&#xff09; &#x1f354;相遇CSDN&#x1f6f8;自媒体&#…

uniapp使用wxml-to-canvas开发小程序保存canvas图片

微信小程序官方解决方案&#xff1a;wxml-to-canvas 使用wxml-to-canvas要知道一些前提条件 1、只能画view&#xff0c;text&#xff0c;image 2、每个元素必须要设置宽高 3、默认是flex布局&#xff0c;可以通过flexDirection: "column"来改变排列方式 4、文字 必…

云服务器搭建GitLab

经验总结&#xff1a; 1、配置需求&#xff1a;云服务器内存最低4G 2、内存4G的云服务器&#xff0c;在运行容器后&#xff0c;会遇到云服务器操作卡顿问题&#xff0c;这里有解决方案 转载&#xff1a;服务器搭建Gitlab卡顿解决办法-CSDN博客 3、云服务器的操作系统会影响…

ROS建图之ROS标准REP-105(官方搬运翻译+个人理解)

REP-105 是一个由 Wim Meeussen 于 2010年10月27日 创建并维护的&#xff0c;名为 "Coordinate Frames for Mobile Platforms"&#xff08;移动平台的坐标系框架&#xff09;的 ROS Enhancement Proposal&#xff08;REP&#xff09;。ROS官方教程&#xff1a;REP 10…

Page 251~254 Win32 GUI项目

win32_gui 源代码&#xff1a; #if defined(UNICODE) && !defined(_UNICODE)#define _UNICODE #elif defined(_UNICODE) && !defined(UNICODE)#define UNICODE #endif#include <tchar.h> #include <windows.h>/* Declare Windows procedure */…
最新文章