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

本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,用中立态度客观事实描述事情本身。
文章结尾有最新热度的文章,感兴趣的可以去看看。
文章有点长(3507字阅读时长:5分),期望您能坚持看完,并有所收获
大家好,今天我继续给大家分享干货。熟悉我的人,都知道我真正的干货一般在中间和末尾部分(文章中间部分:有项目文字和视频介绍)。请耐心看完!谢谢。您可以在文章末尾处获取完整的源代码文件。

作为一名热衷于函数式编程的人,我已经非常习惯那种让开发者工作轻松的酷炫函数式编程风格,于是我开始探索如何在C++ 中以函数式编程风格进行编码。我了解到C++ 在最新版本中有许多惊人的特性。在这篇文章中,我们将学习函数式编程的基础知识,以及在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_eachstd::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++OpenCV实现实时模板匹配算法:老办法匹配已过气,实时识别有何高招?
C++OpenCV实时二维码识别:传统扫码已过时,实时捕捉有何奇招?
C++ OpenCV图像增强:模糊图片成过去,高清蜕变有何妙招?
C++OpenCV手指调音量:传统操作已 out,新解法如何脱颖而出?
C++OpenCV手指检测:为何开发者纷纷放弃传统,拥抱高效新解法
C++OpenCV实现车道偏离检测:为何开发者摒弃复杂监测,采用高效的视觉解决方案
C++OpenCV实现人脸识别:为何开发者正在抛弃旧技术,拥抱CV新解法
原文作者:讳疾忌医-note;如有侵权,请联系管理员删除。