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

news2024/10/5 14:07:25

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';
    else
        goOn = false;
}

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

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

函数对象可能如下所示:

class PrintUntilTenOrMore
{
public:
    PrintUntilTenOrMore() : goOn_(true) {}

    void operator()(int number)
    {
        if (number < 10 && goOn_)
            std::cout << number << std::endl;
        else
            goOn_ = 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';
    else
        goOn = 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';
    else
        goOn = false;
}

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

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

五、总结

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

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

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

在这里插入图片描述

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

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

相关文章

我的文章分类合集目录

文章目录 Java相关基础常规问题类Docker类RabbitMQ类分库分表 网络工程相关路由交换、Cisco Packet TracerIP地址 前端相关数据库 Java相关 基础 Java开发规范、项目开发流程 SpringBoot整合MyBatis实现增删改查(简单,详细) SpringBoot整合MybatisPlus&#xff08;详细&#…

C++ vector类

目录 0.前言 1.vector介绍 2.vector使用 2.1 构造函数(Constructor) 2.1.1. 默认构造函数 (Default Constructor) 2.1.2 填充构造函数 (Fill Constructor) 2.1.3 范围构造函数 (Range Constructor) 2.1.4 拷贝构造函数 (Copy Constructor) 2.2 迭代器(Iterator) 2.2.…

基于移动多媒体信源与信道编码调研

前言 移动多媒体是指在移动通信环境下&#xff0c;通过无线网络传输的音频、视频、图像等多种媒体信息。移动多媒体的特点是数据量大、传输速率高、服务质量要求高&#xff0c;因此对信源编码和信道编码的性能提出了更高的要求。 本文对进3年的移动多媒体信源与信道编码的研究…

信息系统项目管理师0130:工具与技术(8项目整合管理—8.7监控项目工作—8.7.2工具与技术)

点击查看专栏目录 文章目录 8.7.2 工具与技术8.7.2 工具与技术 专家判断监控项目工作过程中,应征求具备如下领域相关专业知识或接受过相关培训的个人或小组的意见,涉及的领域包括:挣值分析;数据的解释和情境化;持续时间和成本的估算技术;趋势分析;关于项目所在的行业以及…

爬虫案例-亚马逊反爬流程分析梳理(验证码突破)(x-amz-captcha)

总体概览&#xff1a;核心主要是需要突破该网站的验证码&#xff0c;成功后会返回我们需要的参数后再去请求一个中间页&#xff08;类似在后台注册一个session&#xff09;&#xff0c;最后需要注意一下 IP 是不能随意切换的 主要难点&#xff1a; 1、梳理整体反爬流程 2、验证…

家政预约小程序01用户注册

目录 1 创建数据源2 创建应用3 创建页面4 用户注册5 角色选择6 设置首页总结 学习低代码的时候&#xff0c;使用官方模板搭建无疑是一个很好的途径。但是低代码工具更新比较频繁&#xff0c;基本每两周就要迭代一个版本。随着官方版本的迭代&#xff0c;官方模板安装好之后会有…

联想端游联运SDK接入指南

1. 接入流程 本文档主要介绍了 联想PC游戏SDK接入流程、联想游戏提供的功能、接入注意事项等。 1.1. 接入方式 1. 联想游戏SDK2.1版本支持“账号防沉迷支付”接入方式&#xff1b; a. 联想提供账号注册、登录等能力 b. 联想提供防沉迷服务 c. 联想提供游戏内支付 1.2. 对…

使用LoRA进行高效微调:基本原理

Using LoRA for efficient fine-tuning: Fundamental principles — ROCm Blogs (amd.com) [2106.09685] LoRA: Low-Rank Adaptation of Large Language Models (arxiv.org) Parametrizations Tutorial — PyTorch Tutorials 2.3.0cu121 documentation 大型语言模型&#xf…

Boyer-Moore投票算法

摩尔投票法&#xff0c;又称为博耶-摩尔多数投票算法&#xff0c;是一种用于在一组数据中寻找多数元素(出现次数超过一半的元素)的算法。该算法的效率非常高&#xff0c;时间复杂度为O(n)&#xff0c;空间复杂度为O(1)&#xff0c;适合处理大数据量的情况。 步骤 首先定义两个…

JSONP原理及应用实例

JSONP是什么 JSONP&#xff08;JSON with Padding&#xff09;是一种跨域数据请求技术&#xff0c;它允许网页在不受同源策略限制的情况下从其他域中请求数据。JSONP的原理是利用 <script> 标签的跨域特性&#xff0c;通过 <script> 标签&#xff0c;指向包含 JSO…

通过继承React.Component创建React组件-5

在React中&#xff0c;V16版本之前有三种方式创建组件&#xff08;createClass() 被删除了)&#xff0c;之后只有两种方式创建组件。这两种方式的组件创建方式效果基本相同&#xff0c;但还是有一些区别&#xff0c;这两种方法在体如下&#xff1a; 本节先了解下用extnds Reac…

vue+elemntui 加减表单框功能样式

<el-form ref"form" :model"form" :rules"rules" label-width"80px"><el-form-item label"配置时间" prop"currentAllocationDate"><div v-for"(item,key) in timeList"><el-date…

ROCm上来自Transformers的双向编码器表示(BERT)

14.8. 来自Transformers的双向编码器表示&#xff08;BERT&#xff09; — 动手学深度学习 2.0.0 documentation (d2l.ai) 代码 import torch from torch import nn from d2l import torch as d2l#save def get_tokens_and_segments(tokens_a, tokens_bNone):""&qu…

Cortex-M3的SysTick 定时器

目录 概述 1 SysTick 定时器 1.1 SysTick 定时器功能介绍 1.2 SysTick 定时器功能实现 1.3 SysTick在系统中的作用 2 SysTick应用的实例 2.1 建立异常服务例程 2.2 使能异常 2.3 闹钟功能 2.4 重定位向量表 2.5 消灭二次触发 3 SysTick在FreeRTOS中的应用 3.1 STM…

(完全解决)Python字典dict如何由键key索引转化为点.dot索引

文章目录 背景解决方案基础版升级版 背景 For example, instead of writing mydict[‘val’], I’d like to write mydict.val. 解决方案 基础版 I’ve always kept this around in a util file. You can use it as a mixin on your own classes too. class dotdict(dict)…

如何进行异地多地兼容组网设置?

跨地区工作、远程办公和异地合作已成为常态。由于网络限制和安全性要求&#xff0c;远程连接仍然是一个具有挑战性的问题。为了解决这一难题&#xff0c;各行各业都在寻找一种能在异地多地兼容的组网设置方案。本文将着重介绍基于【天联】的组网解决方案&#xff0c;探讨其操作…

SpringBoot——整合Thymeleaf模板

目录 模板引擎 新建一个SpringBoot项目 pom.xml application.properties Book BookController bookList.html ​编辑 项目总结 模板引擎 模板引擎是为了用户界面与业务数据分离而产生的&#xff0c;可以生成特定格式的页面在Java中&#xff0c;主要的模板引擎有JSP&…

如何评价刘强东说“业绩不好的人不是我兄弟”

在近日的一次京东管理层会议上&#xff0c;创始人刘强东以不容置疑的口吻表明了对公司文化的坚定态度&#xff1a;“凡是长期业绩不好&#xff0c;从来不拼搏的人&#xff0c;不是我的兄弟。”这句话不仅是对那些工作表现不佳的员工的直接警告&#xff0c;也透露出京东在追求业…

C++语法|多重继承详解(一)|理解虚基类和虚继承

系列汇总讲解&#xff0c;请移步&#xff1a; C语法&#xff5c;虚函数与多态详细讲解系列&#xff08;包含多重继承内容&#xff09; 虚基类是多重继承知识上的铺垫。 首先我们需要明确抽象类和虚基类的区别&#xff1a; 抽象类&#xff1a;有纯虚函数的类 虚基类是什么呢&a…

阿里云的域名购买和备案(一)

前言 本篇文章主要讲阿里云的域名购买和备案。 大家好&#xff0c;我是小荣&#xff0c;我又开始做自己的产品迷途dev了。这里详细记录一下域名购买的流程和备案流程。视频教学 购买流程 1.阿里云官网搜索域名注册 2.搜索你想注册的域名 3.将想要注册的域名加入域名清单 4.点…