C++ STL 函数对象:隐藏的陷阱,如何避免状态带来的麻烦?

STL 函数对象:无状态即无压力

  • 一、简介
  • 二、函数对象
  • 三、避免在函数对象中保存状态
    • 3.1、函数对象
    • 3.2、lambda 表达式
  • 四、选择合适的更高层次的结构
  • 五、总结

一、简介

在使用 C++ 标准模板库 (STL) 时,函数对象 (Function Object) 是一种强大的工具,它可以帮助你编写更具表现力和更健壮的代码。函数对象本质上是可调用对象,它们可以像普通函数一样被调用,但同时可以拥有自己的状态和行为。本文将深入探讨函数对象,并重点讲解如何避免在函数对象中保存状态,从而使你的代码更简洁、更易于维护。

在这里插入图片描述

二、函数对象

先简要回顾一下函数对象。函数对象是一个可以在函数调用语法中使用的对象:

myFunctionObject(x);

即使它是在类(或结构体)中声明的。这种语法是通过声明一个 operator() 运算符实现的:

class MyFunctionObject
{
public:void operator()(int x){....}
};

与简单函数相比,函数对象的优势在于它们可以包含数据:

class MyFunctionObject
{
public:explicit MyFunctionObject(Data data) : data_(data) {}void operator()(int x){//....使用 data_ ....}
private:Data data_;
};

在调用位置:

MyFunctionObject myFunctionObject(data);myFunctionObject(42);

这样,函数调用将使用 42data 来执行。这种类型的对象被称为函数对象。

在 C++11 中,lambda 表达式以更轻量的语法满足了相同的需求:

Data data;
auto myFunctionObject = [data](int x){/*....使用 data....*/};myFunctionObject(42);

自从 C++11 中引入 lambda 表达式后,函数对象的使用频率大大降低,尽管仍然存在一些必须使用函数对象的情况。

函数、函数对象和 lambda 表达式可以使用相同的函数调用语法。因此,它们都是可调用对象。

可调用对象在 STL 中被广泛使用,因为算法具有通用的行为,这些行为由可调用对象定制。以 for_each 为例。for_each 遍历集合中的元素,并对每个元素执行某些操作。这个操作由可调用对象描述。以下示例将集合中的每个数字增加 2,并展示了如何使用函数、函数对象和 lambda 表达式来实现:

使用函数,值 2 必须硬编码:

void bump2(double& number)
{number += 2;
}std::vector<double> numbers = {1, 2, 3, 4, 5};std::for_each(numbers.begin(), numbers.end(), bump2);

使用函数对象,增加的值可以作为参数传递,这提供了更大的灵活性,但语法更繁重:

class Bump
{
public:explicit Bump(double bumpValue) : bumpValue_(bumpValue) {}void operator()(double& number) const{number += bumpValue_;}
private:double bumpValue_;
};std::vector<double> numbers = {1, 2, 3, 4, 5};std::for_each(numbers.begin(), numbers.end(), Bump(2));

lambda 表达式提供了相同的灵活性,但语法更轻量:

std::vector<double> numbers = {1, 2, 3, 4, 5};double bumpValue = 2;
std::for_each(numbers.begin(), numbers.end(),[bumpValue](double& number){number += bumpValue;});

这些示例展示了使用 STL 操作函数对象的语法。现在,以下是如何有效使用它们的准则:避免在其中保存状态。

三、避免在函数对象中保存状态

在使用 STL 的初期,可能会很想在函数对象中使用数据成员变量来保存状态。例如,用于存储在遍历集合过程中更新的当前结果,或用于存储哨兵值。

尽管 lambda 表达式在标准情况下取代了函数对象,但许多代码库仍在赶上 C++11,还没有使用 lambda 表达式。此外,仍然存在一些只能通过函数对象解决的情况。因此,本文将涵盖函数对象和 lambda 表达式,特别是看看如何将避免状态的准则应用于两者。

3.1、函数对象

示例:统计集合 numbers 中值 7 出现的次数。

class Count7
{
public:Count7() : counter_(0) {}void operator()(int number){if (number == 7) ++counter_;}int getCounter() const {return counter_;}
private:int counter_;
};

在调用位置,函数对象可以这样使用:

std::vector<int> numbers = {1, 7, 4, 7, 7, 2, 3, 4};int count = std::for_each(numbers.begin(), numbers.end(), Count7()).getCounter();

在这里,实例化一个 Count7 类型的函数对象,并将其传递给 for_each(搜索的数字可以在函数对象中参数化,以便能够编写 Count(7),但这并不是重点。相反,更想关注函数对象中维护的状态)。for_each 将传递的函数对象应用于集合中的每个元素,然后返回它。这样,就可以在 for_each 返回的匿名函数对象上调用 getCounter() 方法。

这段代码的复杂性暗示着它的设计存在问题。这里的问题是函数对象有一个状态:它的成员 counter_,而函数对象不适合保存状态。为了说明这一点,你可能想知道:为什么使用 for_each 返回值的这个相对不为人知的特性?为什么不简单地编写以下代码:

std::vector<int> numbers = {1, 7, 4, 7, 7, 2, 3, 4};Count7 count7;
std::for_each(numbers.begin(), numbers.end(), count7);int count = count7.getCounter();

这段代码创建了计数函数对象,将其传递给 for_each,并检索计数结果。这段代码的问题在于它根本无法工作。如果尝试编译它,会发现 count 中的值是 0。这是为什么呢?

原因是,count7 从未进入 for_each 的内部。实际上,for_each 按值获取其可调用对象,因此 for_each 使用的是 count7 的副本,并且该副本的状态已被修改。

这是应该避免在函数对象中保存状态的第一个原因:状态会丢失。

这在上面的示例中很明显,但它不止于此:for_each 的特殊之处在于它在整个集合遍历过程中始终保持相同的函数对象实例,但并非所有算法都是如此。其他算法不保证它们在遍历集合的过程中会使用相同的可调用对象实例。因此,可调用对象的实例可能会在算法执行过程中被复制、赋值或销毁,从而导致状态无法维护。要确切了解哪些算法提供了这种保证,可以查看标准,但一些非常常见的算法(如 std::transform)却没有。

现在,应该避免在函数对象中保存状态的另一个原因是:它会使代码变得更加复杂。大多数情况下,存在更好的、更干净、更具表现力的方法。这也适用于 lambda 表达式。

3.2、lambda 表达式

使用 lambda 表达式的代码,统计 numbers 中数字 7 出现的次数:

std::vector<int> numbers = {1, 7, 4, 7, 7, 2, 3, 4};int count = 0;
std::for_each(numbers.begin(), numbers.end(),[&count](int number){ if (number == 7) ++count;});std::cout << count << std::endl;

这段代码调用 for_each 来遍历整个集合,并在每次遇到 7 时递增变量 counter(按引用传递给 lambda 表达式)。

这段代码不好,因为它用于执行的任务过于复杂。它展示了通过暴露其状态来计数元素的技术方法,而它应该简单地说明它正在统计集合中的 7,任何实现状态都应该被抽象掉。这实际上与尊重抽象层次的原则相一致,这是编程最重要的原则。

那么该怎么办呢?

四、选择合适的更高层次的结构

有一种简单的方法可以重写上面的特定示例,并且与所有版本的 C++ 兼容。它包括将 for_each 移除,并用 count 替换它,因为 count 专门用于此任务:

std::vector<int> numbers = {1, 7, 4, 7, 7, 2, 3, 4};int count = std::count(numbers.begin(), numbers.end(), 7);

当然,这并不意味着永远不需要函数对象或 lambda 表达式 —— 确实需要它们。但这里想要传达的信息是,如果发现自己在函数对象或 lambda 表达式中需要状态,那么应该重新考虑正在使用的更高层次的结构。可能存在一个更适合所需解决的问题的结构。

看看另一个在可调用对象中保存状态的经典示例:哨兵值。

哨兵值是一个用于预期算法终止的变量。例如,在以下代码中,goOn 是哨兵值:

std::vector<int> numbers = {8, 4, 3, 2, 10, 4, 2, 7, 3};bool goOn = true;
for (size_t n = 0; n < numbers.size() && goOn; ++n) {if (numbers[n] < 10)std::cout << numbers[n] << '\n';elsegoOn = false;
}

这段代码的目的是打印集合中的数字,只要它们小于 10,并在遍历过程中遇到 10 时停止。

当重构这段代码以利用 STL 的表现力时,可能会很想将哨兵值作为函数对象/lambda 表达式中的状态保存。

函数对象可能如下所示:

class PrintUntilTenOrMore
{
public:PrintUntilTenOrMore() : goOn_(true) {}void operator()(int number){if (number < 10 && goOn_)std::cout << number << std::endl;elsegoOn_ = false;}private:bool goOn_;
};

在调用位置:

std::vector<int> numbers = {8, 4, 3, 2, 10, 4, 2, 7, 3};
std::for_each(numbers.begin(), numbers.end(), PrintUntilTenOrMore());

使用 lambda 表达式的类似代码如下所示:

std::vector<int> numbers = {8, 4, 3, 2, 10, 4, 2, 7, 3};bool goOn = true;
std::for_each(numbers.begin(), numbers.end(), [&goOn](int number)
{if (number < 10 && goOn)std::cout << number << '\n';elsegoOn = false;
});

但是,这些代码片段存在几个问题:

  • 状态 goOn 使它们变得复杂:需要一些时间才能在脑海中理清它的作用。
  • 调用位置存在矛盾:它说它对每个元素执行某些操作,但也说它不会在 10 之后继续执行。

有很多方法可以解决这个问题。一种方法是使用 find_if 将测试从 for_each 中移除:

auto first10 = std::find_if(numbers.begin(), numbers.end(), [](int number){return number >= 10;});
std::for_each(numbers.begin(), first10, [](int number){std::cout << number << std::endl;} );

不再有哨兵值,不再有状态。

这在这种情况下效果很好,但如果需要根据转换结果进行过滤,例如将函数 f 应用于数字的结果呢?也就是说,如果初始代码是:

std::vector<int> numbers = {8, 4, 3, 2, 10, 4, 2, 7, 3};bool goOn = true;
for (size_t n = 0; n < numbers.size() && goOn; ++n)
{int result = f(numbers[n]);if (result < 10)std::cout << result << '\n';elsegoOn = false;
}

那么要使用 std::transform 而不是 std::for_each。但在这种情况下,find_if 也需要对每个元素调用 f,这是没有意义的,因为对每个元素应用两次 f,一次在 find_if 中,一次在 transform 中。

这里的一个解决方案是使用范围(Range)。

五、总结

本文深入探讨了 STL 函数对象,并重点讲解了避免在函数对象中保存状态的重要性。通过使用更高级的 STL 算法,例如 countfind_if,可以避免使用函数对象来管理状态,从而使代码更简洁、更易于维护。

记住,函数对象应该专注于执行特定的操作,而不是管理状态。

希望本文能帮助你更好地理解 STL 函数对象,并学会编写更优雅、更健壮的 C++ 代码。

在这里插入图片描述

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

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

相关文章

02324 自学考试 离散数学屈婉玲教材 目录

02324 自学考试 离散数学屈婉玲教材 目录 02324 自学考试 离散数学屈婉玲教材 02324离散数学全程班历年真题资料

技术面试,项目实战,求职利器

之前找工作一直想找一个能真正系统性学开发的地方&#xff0c;之前毕业找工作的时候无意间碰到下面这个网站&#xff0c;感觉还挺不错的&#xff0c;用上面的技术实战内容应对技术面试&#xff0c;也算是求职利器了。有需要的可以自取&#xff1a; https://how2j.cn?p156336 实…

VMware虚拟机中ubuntu使用记录(10)—— 如何在Ubuntu18.04中使用自己的单目摄像头运行ORB_SLAM3(亲测有效,踩坑记录)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、ORB_SLAM3源码编译二、ORB_SLAM3实时单目相机测试1. 查看摄像头的话题2. 运行测试 三. 运行测试可能的报错1. 报错一(1) 问题描述(2) 原因分析(3) 解决 2. …

SWM320系列应用

一、Swm320系列 SPI 应用 现象&#xff1a;应用SWM320的SPI1的模式0作为从机&#xff0c;整体产品硬件平台同步上电&#xff0c;从机的SPI无法正常工作&#xff0c;进不了中断&#xff0c;手工复位一次或连接SWD调试就正常了&#xff0c;这样的情况应该怎么解决&#xff1f;其…

Windows远程连接命令?

Windows操作系统提供了多种远程连接命令&#xff0c;使用户可以通过网络连接到远程计算机&#xff0c;并在远程操作系统上执行操作。远程连接命令可方便实现远程工作、故障排查和系统维护等任务。本文将介绍几种常见的Windows远程连接命令及其基本使用方法。 远程连接命令 Win…

C语言 | Leetcode C语言题解之第112题路径总和

题目&#xff1a; 题解&#xff1a; bool hasPathSum(struct TreeNode *root, int sum) {if (root NULL) {return false;}if (root->left NULL && root->right NULL) {return sum root->val;}return hasPathSum(root->left, sum - root->val) ||ha…

从0开始带你成为Kafka消息中间件高手---第二讲

从0开始带你成为Kafka消息中间件高手—第二讲 那么在消费数据的时候&#xff0c;需要从磁盘文件里读取数据后通过网络发送出去&#xff0c;这个时候怎么提升性能呢&#xff1f; 首先就是利用了page cache技术&#xff0c;之前说过&#xff0c;kafka写入数据到磁盘文件的时候&…

企业微信hook接口协议,ipad协议http,根据手机号搜索联系人

根据手机号搜索联系人 参数名必选类型说明uuid是String每个实例的唯一标识&#xff0c;根据uuid操作具体企业微信 请求示例 {"uuid":"3240fde0-45e2-48c0-90e8-cb098d0ebe43","phoneNumber":"1357xxxx" } 返回示例 {"data&q…

隐私是建立人工智能信任的关键

微信关注公众号网络研究观获取更多。 谷歌的 Astra 是其首款人工智能代理 谷歌继续将生成式人工智能融入网络安全 云的复杂性是我们这个时代最大的安全威胁 云安全最受关注的问题&#xff1a;人工智能生成的代码 企业可以从人工智能中获得转型利益&#xff0c;但确保“隐…

CAD二次开发(4)-编辑图形

工具类&#xff1a;EditEntityTool.cs using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Geometry; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Th…

Leetcode | 5-22 | 每日一题 | 找出输掉零场或一场比赛的玩家

&#x1f525;博客介绍&#xff1a; EvLast &#x1f3a5;系列专栏&#xff1a; 数据结构与算法 , 算法入门 , C项目 , Leetcode_DayCode &#x1f3a5; 当前专栏: Leetcode_DayCode 专题 : 数据结构帮助小白快速入门算法 &#x1f44d;&#x1f44d;&#x1f44d;&#x1…

Go语言之Gorm框架(一) ——初窥Gorm框架

Gorm和Mysql驱动的安装 打开终端&#xff0c;输入下列命令即可&#xff1a; go get gorm.io/driver/mysql go get gorm.io/gormGorm连接数据库 示例 package mainimport ("fmt""github.com/sirupsen/logrus""gorm.io/driver/mysql""gor…

HE TB PPDU MU-RTS

看起来像是MU-RTS的触发帧的应答不是HE TB PPDU&#xff0c;而是传统得的帧&#xff0c;应答CTS。 非AP 的STA&#xff0c;是不能发送触发帧&#xff0c;也就是说&#xff0c;触发帧&#xff0c;只能是由AP发送给STA

AI视频智能分析引领智慧园区升级:EasyCVR智慧园区视频管理方案

一、系统概述与需求 随着信息技术的不断发展&#xff0c;智慧园区作为城市现代化的重要组成部分&#xff0c;对安全监控、智能化管理提出了更高的要求。智慧园区视频智能管理系统作为实现园区智能化管理的重要手段&#xff0c;通过对园区内各关键节点的视频监控和智能分析&…

一文了解安卓内存抖动

目录 目录一、什么是内存抖动&#xff1f;1.1 Android里的内存抖动1.2 如何直观查看这种现象1.3 内存抖动带来的风险 二、如何避免内存抖动 目录 一、什么是内存抖动&#xff1f; 在程序里&#xff0c;每创建一个对象&#xff0c;就会有一块内存分配给它&#xff0c;每分配一…

LabVIEW虚拟测试实验室开发

LabVIEW虚拟测试实验室开发 在当代的科技和工业进步中&#xff0c;测试与测量扮演着至关重要的角色。随着技术的发展&#xff0c;测试系统也变得日益复杂和成本昂贵&#xff0c;同时对测试结果的准确性和测试过程的效率要求越来越高。开发了一种基于LabVIEW的虚拟测试实验室的…

ICQ 将于 6 月关闭,这是一种奇怪的方式,发现它在 2024 年仍然活跃

你知道ICQ还活着吗&#xff1f;不过&#xff0c;不要太兴奋;它将永远消失。 还记得ICQ吗&#xff1f;如果你这样做了&#xff0c;你可能会记得它是AOL在1998年购买的Messenger客户端&#xff0c;就在Yahoo Instant Messager和MSN Messenger加入竞争的时候。然后Skype出现了&…

SpringBoot3笔记(一)SpringBoot3-核心特性

快速学习 SpringBoot 看官方文档&#xff1a; Spring Boot Reference Documentation 计划三天学完 笔记&#xff1a;https://www.yuque.com/leifengyang/springboot3 代码&#xff1a;https://gitee.com/leifengyang/spring-boot-3 一、SpringBoot3 - 快速入门 1.1 简介 …

【全开源】招聘求职小程序系统源码(ThinkPHP+原生微信小程序)

基于ThinkPHP和原生微信小程序开发的招聘平台系统&#xff0c;包含微信小程序求职者端、微信小程序企业招聘端、PC企业招聘端、PC管理平台端 构建高效人才交流平台 一、引言&#xff1a;招聘求职市场的数字化趋势 在数字化时代&#xff0c;招聘求职市场也迎来了巨大的变革。…