ITPub博客

首页 > 数据库 > 数据库开发技术 > C++函数式编程:那些你不容错过的精彩技巧

C++函数式编程:那些你不容错过的精彩技巧

原创 数据库开发技术 作者:讳疾忌医-note 时间:2025-02-04 01:01:11 0 删除 编辑
创作不易,方便的话点点关注,谢谢
本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,用中立态度客观事实描述事情本身。

文章结尾有最新热度的文章,感兴趣的可以去看看。

文章有点长(3507字阅读时长:5分),期望您能坚持看完,并有所收获


大家好,今天我继续给大家分享干货。熟悉我的人,都知道我真正的干货一般在中间和末尾部分(文章中间部分:有项目文字和视频介绍)。请耐心看完!谢谢。您可以在文章末尾处获取完整的源代码文件。

 

作为一名热衷于函数式编程的人,我已经非常习惯那种让开发者工作轻松的酷炫函数式编程风格,于是我开始探索如何在C++ 中以函数式编程风格进行编码。我了解到C++ 在最新版本中有许多惊人的特性。在这篇文章中,我们将学习函数式编程的基础知识,以及在C++ 中可以实现其中哪些部分。

前置条件

  • • 有一些C++ 编程经验
  • • 对任何一种函数式编程语言或范式有一定经验
  • • 一个支持C++11 标准的C++ 编译器

让我们开始吧

函数式编程是一种声明式的编程风格。它唯一关注的是“解决什么问题”,而与之相对的命令式编程风格主要关注的是“如何解决问题”。函数式编程使用表达式而非语句。

函数式编程关键概念

  • • 函数作为一等公民
  • • 纯函数

函数式编程规则

  • • 不可变变量:在函数式编程中,变量初始化后就不能再修改。就是不可以。你可以创建新变量,但不能修改已有的变量。
  • • 无副作用:副作用是指当前正在执行的函数之外的某些状态发生改变。修改函数外部定义的变量、输出到控制台、抛出异常以及从文件中读取数据,这些都是副作用的例子。
  • • 无状态:函数内部可能有包含临时状态的局部变量,但该函数不能引用它所属类或对象的任何成员变量。状态会导致可变性,进而引发副作用。函数式编程不希望你这么做。

这些是函数式编程的一些关键概念和规则。即使你并非一直遵循所有这些规则,也仍然可以在应用程序中从函数式编程的理念中受益。总之,C++ 从来就不是一门严格或纯粹的函数式编程语言。说实话,函数式编程并非对每个问题都是合适的工具(是的!这是我的观点。未来可能会改变。观点会随着经验而改变!)。目前,让我们来了解函数式编程擅长解决哪些问题。

函数作为一等公民

在函数式编程领域,函数是一等公民。函数被当作与其他任何变量一样对待。例如,一个函数可以作为参数传递给其他函数,也可以赋值给一个变量。

在C++ 中,函数并非一等公民。与之最接近的是lambda 表达式。

auto printText = [](std::string text) { std::cout << text << std::endl; };

上述代码创建了一个lambda 表达式printText,它接受一个参数text,将其打印出来且不返回任何值。[]括号用于指定函数的闭包。关于lambda 表达式的更多信息可以在此处找到。

让我们通过查看如何在C++ 的vector或任何集合上应用诸如filter(过滤)、map(映射)、reduce(归约)等不同的组合子,来了解如何进行函数式编程。以下面这个vector为例:

std::vector<std::string> messages = { "Hello Pal", "How are you?", "I'm still coding in C++" };

for_each

std::for_each(messages.begin(), messages.end(), printText);

前两个参数是集合的起始和结束位置。然后,我们传递的第三个参数是一个一元lambda 表达式,它对每个元素进行操作。

让我们创建一个自定义的Student对象的vector,并对其应用这些组合子。

class Student
{
public:
    string _name;
    int _score;
    Student(string name, int score)
    {
        _name = name;
        _score = score;
    }
    void incrementScore() {
        _score += 1;
    }
    string name()
    
{
        return _name;
    }
    int score()
    
{
        return _score;
    }
};
std::vector<Student> students = {Student("Alice", 85), Student("Bob", 62), Student("Charlie", 81), Student("Jack", 90), Student("Jimmy", 40), Student("Sherlock", 67),};

假设你是老师,想打印学生的详细信息。那么,你可以使用for_each来打印他们的信息。

auto printStudentDetails = [](Student student) { std::cout << student.name() << " " << student.score() << std::endl; };
std::for_each(students.begin(), students.end(), printStudentDetails);

在上述代码中,我们使用了一个打印lambda 表达式来打印每个学生的详细信息。

map

在C++ 中,与函数式编程中的map等价的是transform。假设你出了一道一分的题目,但数据不充分或无效,你想给每个学生加一分。那么你可以使用transform来更新每个学生的分数。

auto addOne = [](Student student) { student.incrementScore(); return student; };
std::transform(students.begin(), students.end(), students.begin(), addOne);

你需要提供输入集合的起始和结束指针、输出集合的起始指针以及操作。你甚至可以同时对两个集合执行transform操作。你可以自行研究一下,这超出了本博客的范围。

filter

在C++ 中,函数式编程的filter根据用例有多种等价实现。如果你只是想获取具有特定属性的元素的副本,可以使用copy_if。此外,还有其他函数,如remove_if、find_if等。而且每个这样的函数都有对应的“非”操作版本,如copy_if_not、find_if_not等。

auto aboveEighty = [](Student student) { return student.score() > 80; };
vector<Student> topStudents = {};
auto it = std::copy_if(students.begin(), students.end(), back_inserter(topStudents), aboveEighty);
std::for_each(topStudents.begin(), topStudents.end(), printStudentDetails);

copy_if的语法是:copy_if(Iterator inputBegin, Iterator inputEnd, Iterator outputBegin, predicate)。你需要提供输入集合的起始和结束指针、输出集合的起始指针以及操作。我使用了back_inserter_iterator而不是outputBegin,这样我就可以将元素插入到topStudents向量中,而无需初始化该向量。

Reduce 或 Fold

在C++ 中,与函数式编程的reduce等价的是accumulate。假设你有一个包含数字的向量,并且想将它们全部相加。

vector<int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << std::accumulate(numbers.begin(), numbers.end(), 0,
        [](int first, int second) { return  first + second; });

你需要提供输入集合的起始和结束指针、累加器的初始值以及一个二元lambda 表达式。累加器也可以应用于我们自定义的students向量。下面的代码计算班级的平均分数。

auto binaryOp = [](int score, Student student) { return score + student.score(); };
std::cout << std::accumulate(students.begin(), students.end(), 0, binaryOp)/students.size();

纯函数

如果一个函数满足以下条件,它就是一个纯函数:

  • • 函数的执行没有副作用。
  • • 函数的返回值仅取决于传递给函数的输入参数。
int sum(int a, int b){
   return a+b;
}

上面创建的函数是一个纯函数。C++ 中的许多内置函数都是纯函数,如min、max、strlen等。你可以编写接受所有const参数且不改变实例变量的函数。在GCC 中,你甚至可以使用“pure”属性将函数标记为纯函数,这有助于更好地优化。如果编译器知道一个函数是纯函数,那么可以对其应用循环优化和子表达式消除。但默认情况下,C++ 也允许副作用和可变性。

public class NonPureFunctionAndMutatingParametersExample{
    private int total= 0;

    public int add(int nextValue) {
        this.total+= nextValue;
        return this.total;
    }
    public void incrementScore(Student &student){
         student.score += 1;
    }
}

在上述代码中:

  • • add方法改变了状态。
  • • incrementScore方法改变了函数参数。

除非必要,我们应该避免这样做。是的,在某些情况下,副作用和可变性是有益的。

不可变变量这条规则是一个值得遵循的良好实践。一旦你创建了一个变量并设置了它的值,你就可以完全放心,该变量的值永远不会改变,但有时这并不适用。如果你正在构建一个要在终端用户的低配置机器上运行的应用程序,那么你将面临有限的内存和时间。在这种情况下,建议接受参数的引用/指针,对它们进行修改以节省内存和复制时间。

无副作用这条规则非常有用。它让我们确信这个函数不会影响外部世界,并且在任何地方调用它都是安全的。但在某些场景下,这会带来困难,例如写入数据库(这就是一种副作用)。

还有一些其他概念和规则,如高阶函数、使用递归代替循环等,可在需要时应用。

结论

正如我之前所说,函数式编程是一种很棒的范式,但在某些情况下,不遵循它的一些规则反而能得到最优解。

在过去几年中,C++ 发生了很大的变化。新的结构使其成为构建任何类型应用程序的出色工具。它不再仅仅是“带类的C”。编码愉快!!!以上就是我的分享。这些分析皆源自我的个人经验,希望上面分享的这些东西对大家有帮助,感谢大家!

参考文献

1、ISO/IEC 14882:2011 《Programming Language C++》

2、Alexandrescu, A. 《Modern C++ Design: Generic Programming and Design Patterns Applied》

3、Meyers, S. 《Effective C++: 55 Specific Ways to Improve Your Programs and Designs》

 

文章中源代码怎么获取:关注官方微信公众号,就可以获取。

点个“在看”不失联

最新热门文章推荐:

借助C++OpenCV图像背景替换:老办法束手无策,新方案如何出奇制胜?

DeepSeek优势探秘:传统训练成本高,它如何凭优势改写大模型发展走向?

从无名到C++大神,赫布·萨特(Herb Sutter)如何逆袭?

C++游戏开发:传统贪吃蛇,如何玩出新花样?

告别传统拜年,C++ncurses库终端拜年程序

C++OpenCV实现实时模板匹配算法:老办法匹配已过气,实时识别有何高招?

C++OpenCV实时二维码识别:传统扫码已过时,实时捕捉有何奇招?

C++ OpenCV图像增强:模糊图片成过去,高清蜕变有何妙招?

C++OpenCV手指调音量:传统操作已 out,新解法如何脱颖而出?

C++OpenCV手指检测:为何开发者纷纷放弃传统,拥抱高效新解法

C++OpenCV实现车道偏离检测:为何开发者摒弃复杂监测,采用高效的视觉解决方案

C++OpenCV实现人脸识别:为何开发者正在抛弃旧技术,拥抱CV新解法

原文作者:讳疾忌医-note;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2024-08-12

  • 博文量
    126
  • 访问量
    73110