首页 > 编程语言 >C++ STL 函数对象:隐藏的陷阱,如何避免状态带来的麻烦?

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

时间:2024-05-26 10:29:24浏览次数:34  
标签:std 函数 STL C++ 对象 numbers 陷阱 each lambda

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

一、简介

在使用 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++ 代码。

在这里插入图片描述

标签:std,函数,STL,C++,对象,numbers,陷阱,each,lambda
From: https://blog.csdn.net/Long_xu/article/details/139203128

相关文章

  • 【c++游戏】harry potter(破解版)
    引子相信——这款哈利波特游戏大家一定都见过,作为最流行的哈利波特文字游戏之一,其改变参数的密码实在是让人头疼,而且还要费劲去翻源代码,如下展示的代码是已经删除了改变参数要填密码的机制,真正做到破解版,同时,作者也对代码进行了改进,直接看源代码!(代码行数多,复制慢,请多等一会)游......
  • 计算机毕业设计项目推荐,82131基于SSM的流浪动物救助网站的设计与实现(开题答辩+程序定
    SSM流浪动物救助网站摘要随着生活水平的持续提高和家庭规模的缩小,宠物已经成为越来越多都市人生活的一部分,随着宠物的增多,流浪的动物的日益增多,中国的流浪动物领养和救助也随之形成规模,同时展现巨大潜力。本次系统的是基于SSM框架的流浪动物救助网站管理系统,平台用户可以......
  • (免费领源码)Java/Mysql数据库+53102互联网美食分享平台,计算机毕业设计项目推荐上万套实
    springboot互联网互联网美食分享平台系   院XXXX学科门类XXX专   业 XXX班级XXX学   号XXX姓   名XXX指导菜谱大全 XXX菜谱大全职称XXX2023年2月摘 要大数据时代下,数据呈爆炸式地增长。为了迎合信息化时代的潮流和信息化......
  • (免费领源码)Java/Mysql数据库+53135高校大学生学科竞赛管理系统,计算机毕业设计项目推荐
    springboot高校大学生学科竞赛管理系统的设计与实现系   院XXXX学科门类XXX专   业 XXX班级XXX学   号XXX姓   名XXX2023年4月摘 要随着互联网趋势的到来,各行各业都在考虑利用互联网将自己推广出去,最好方式就是建立自己的互联......
  • (免费领源码)Java/Mysql数据库+53233基于SpringBoot的社区疫情防控系统,计算机毕业设计项
    springboot社区疫情防控管理系统摘 要信息化社会内需要与之针对性的信息获取途径,但是途径的扩展基本上为人们所努力的方向,由于站在的角度存在偏差,人们经常能够获得不同类型信息,这也是技术最为难以攻克的课题。针对社区疫情防控管理系统等问题,对社区疫情防控管理系统进行研......
  • Effective ModernC++条款42:考虑使用置入代替插入
    更多C++学习笔记,关注wx公众号:cpp读书笔记Item42:Consideremplacementinsteadofinsertion如果你拥有一个容器,例如放着std::string,那么当你通过插入(insertion)函数(例如insert,push_front,push_back,或者对于std::forward_list来说是insert_after)添加新元素时,你传入的元......
  • (免费领取源码)计算机毕业设计项目:07558基于Python的校园宿舍(开题答辩+程序定制+全套文
    摘要本论文主要论述了如何使用django开发一个校园宿舍管理系统,本系统将严格按照软件开发流程进行各个阶段的工作,采用B/S架构,面向对象编程思想进行项目开发。在引言中,作者将论述校园宿舍管理系统的当前背景以及系统开发的目的,后续章节将严格按照软件开发流程,对系统进行各......
  • AcWing 3466. 清点代码库(STL:map,vector)
    3466.清点代码库需要求有几种不同数列,每种有多少个,可以想到用map。它的键是一个数列,可以把它放在vector里。也就是map<vector<int>,int>要满足要求的输出序列,就要想把它放在其他容器,或数组里,进行排序。因为map不能自定义排序,而且既要对值排序,还要对键排序。我起初是定......
  • 深入理解C++智能指针系列(一)
    引言都知道C/C++的最难的就是需要程序员自己管理内存,往往会因为一个简单的逻辑错误导致内存管理异常。通常内存管理过程中会遇到以下问题:内存泄漏:当开发者忘记释放已分配的内存时,就会发生内存泄漏。这种情况在大型项目中非常常见,项目中存在大量动态内存操作时,很容易遗漏......
  • Ubuntu16.04 opencv环境搭建(C++)
    Ubuntu下vscode跑opencv程序环境搭建。目录1ubuntu查看opencv版本2下载opencv包3依赖配置4进入安装包内执行5配置环境变量6VScode配置-下载c++扩展7编译运行helloworld8在vscode中配置opencv环境9运行结果1ubuntu查看opencv版本pkg-config--modversion......