《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.mzph.cn/news/616287.shtml

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

相关文章

x-cmd pkg | czg - git commit 智能生成工具

目录 简介首次用户功能特点竞品和相关作品进一步探索 简介 czg 源于 commitizen/cz-cli 交互插件中 cz-git 的延伸项目&#xff0c;重新使用 TypeScript 编写的零依赖独立的 Node.js 命令行工具。旨在使用交互友好的方式&#xff0c;辅助用户生成规范的 git commit message 约…

游泳耳机哪种款式好?最值得入手的游泳耳机大全

在如今注重健康和娱乐的生活方式中&#xff0c;游泳作为一项全身性的运动备受欢迎。然而&#xff0c;对于热爱水中活动的人们来说&#xff0c;选择一款出色的游泳耳机至关重要。好的游泳耳机不仅能提供清晰的音质&#xff0c;还能有效防水&#xff0c;让您在水中尽情畅游的同时…

MySQL中约束是什么?

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;重拾MySQL &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出现错误&am…

记录由客户端http请求原因引起的5xx响应问题排查过程

看到 http 状态码 5xx&#xff0c;很多开发者第一感觉就是服务端的问题&#xff0c;其实并不全是。下面我遇到的问题就是一个例外。 问题描述 最近在为反向代理 nginx 配置 auth_request 后&#xff0c;出现了请求504错误。 504状态码是HTTP协议中的一种服务器错误状态码。当…

Windows压缩包的MySQL安装方式

1.下载压缩包 https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.35-winx64.zip 2.解压压缩包&#xff08;建议将解压到非C盘&#xff0c;路径不要出现特殊符号&#xff09; 3.在MySQL主目录下&#xff0c;创建my.ini空文件&#xff08;先创建一个txt文件&#xff0c;进…

JavaScript删除数组中指定元素的5种方法

文章目录 目录 文章目录 前言 一、数组是什么&#xff1f; 二、讲解数组 总结 前言 在JavaScript开发中&#xff0c;处理数组是一项非常常见的任务。有时候我们需要从数组中删除特定的元素&#xff0c;以便对数组进行进一步操作或者满足特定的需求。幸运的是&#xff0c;JavaS…

Swoft - Bean

一、Bean 在 Swoft 中&#xff0c;一个 Bean 就是一个类的一个对象实例。 它(Bean)是通过容器来存放和管理整个生命周期的。 最直观的感受就是省去了频繁new的过程&#xff0c;节省了资源的开销。 二、Bean的使用 1、创建Bean 在【gateway/app/Http/Controller】下新建一个名为…

7.vue学习笔记(模板引用+组件组成+组件嵌套关系)

文章目录 1.模板引用2.组件组成3.组件嵌套关系3.1.App.vue3.1.1.Header.vue3.1.2.Main.vue3.1.3.Aside.vue 1.模板引用 在Vue直接读取DOM 虽然Vue的声明性渲染模型为你抽象了大部分对DOM的直接操作&#xff08;事件&#xff0c;内容&#xff0c;属性&#xff09;&#xff0c; …

持久双向通信网络协议-WebSocket-入门案例实现demo

1 介绍 WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手&#xff0c;两者之间就可以创建持久性的连接&#xff0c; 并进行双向数据传输。 HTTP协议和WebSocket协议对比&#xff1a; HTTP是短连接&#xff0…

【MATLAB】小波_LSTM神经网络时序预测算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 小波-LSTM神经网络时序预测算法是一种结合了小波变换和长短期记忆神经网络&#xff08;LSTM&#xff09;的时间序列预测方法。 小波变换是一种信号处理方法&#xff0c;能够将信号分解为…

<蓝桥杯软件赛>零基础备赛20周--第14周--BFS

报名明年4月蓝桥杯软件赛的同学们&#xff0c;如果你是大一零基础&#xff0c;目前懵懂中&#xff0c;不知该怎么办&#xff0c;可以看看本博客系列&#xff1a;备赛20周合集 20周的完整安排请点击&#xff1a;20周计划 每周发1个博客&#xff0c;共20周。 在QQ群上交流答疑&am…

NowinAndroid—2024 Android现代开发全功能应用

NowinAndroid—2024 Android现代开发全功能应用 现代Android开发全功能示例应用Now-in-Android&#xff0c;它是用Kotlin和Jetpack Compose开发的&#xff0c;功能非常强大。这个应用遵循了安卓设计和开发的最佳方法&#xff0c;旨在给开发者提供实用的参考资料。无论你是新手…

2024年甘肃省职业院校技能大赛信息安全管理与评估 样题一 模块一

竞赛需要完成三个阶段的任务&#xff0c;分别完成三个模块&#xff0c;总分共计 1000分。三个模块内容和分值分别是&#xff1a; 1.第一阶段&#xff1a;模块一 网络平台搭建与设备安全防护&#xff08;180 分钟&#xff0c;300 分&#xff09;。 2.第二阶段&#xff1a;模块二…

002 Golang-channel-practice

第二题&#xff1a; 创建一个生产器和接收器&#xff0c;再建立一个无缓冲的channel。生产器负责把数据放进管道里&#xff0c;接收器负责把管道里面的数据打印出来。这里我们开5个协程把数据打印出来。 直接上代码&#xff01; package mainimport ("fmt" )func …

“人工智能”领域的高含金量证书接受报名!

由国家工信部权威认证的人工智能证书是跨入人工智能行业的敲门砖&#xff0c;随着人工智能技术的发展越来越成熟&#xff0c;相关的从业人员也会剧增&#xff0c;证书的考取难度也会变高。如果已经从事或者准备从事人工智能行业的人员&#xff0c;对于考证宜早不宜迟&#xff0…

作业--day43

使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数&#xff0c;将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#xff0c…

git常用命令及概念对比

查看日志 git config --list 查看git的配置 git status 查看暂存区和工作区的变化内容&#xff08;查看工作区和暂存区有哪些修改&#xff09; git log 查看当前分支的commit 记录 git log -p commitID详细查看commitID的具体内容 git log -L :funcName:fileName 查看file…

Springboot注解@EnableConfigurationProperties和@ConfigurationProperties关系和作用

目录 EnableConfigurationProperties和ConfigurationProperties关系是什么&#xff1f; 简介 ConfigurationProperties EnableConfigurationProperties 二者之间的联系 总结 EnableConfigurationProperties和ConfigurationProperties关系是什么&#xff1f; 其实我能明白…

利用矩阵特征值解决微分方程【1】

目录 一. 特征值介绍 二. 单变量常微分方程 三. 利用矩阵解决微分方程问题 四. 小结 4.1 矩阵论 4.2 特征值与特征向量内涵 4.3 应用 一. 特征值介绍 线性代数有两大基础问题&#xff1a; 如果A为对角阵的话&#xff0c;那么问题就很好解决。需要注意的是&#xff0c;矩…

Springboot药物不良反应智能监测系统源码

一、系统简介 ADR指的是药品不良反应&#xff0c;即在合格药品在正常用法用量下&#xff0c;出现与用药目的无关或意外的有害反应。ADR数据辨别引擎、药品ADR信号主动监测引擎、ADR处置行为分析引擎。ADR数据辨别引擎&#xff0c;通过主动监测患者具象临床指标&#xff0c;比如…