当前位置: 首页 > article >正文

C++ 越来越像函数式编程了!

C++ 越来越像函数式编程了

大家好,欢迎来到今天的博客话题。今天我们要聊的是 C++ 这门老牌的强类型语言是如何一步一步向函数式编程靠拢的。从最早的函数指针,到函数对象(Functor),再到 std::functionstd::bind,还有 lambda 表达式,最后我们重点讲讲 C++20 的 Ranges。这一路走来,C++ 变得越来越强大,越来越像函数式编程了。

c++20-functional-programming

什么是函数式编程?

在深入探讨 C++ 的演变之前,我们先简单介绍一下什么是函数式编程(Functional Programming)。函数式编程是一种编程范式,它把计算视为数学函数的求值,强调引用透明性纯函数高阶函数惰性求值等概念。

  • 引用透明性:相同输入总是得到相同的输出,没有副作用。
  • 纯函数:函数内部不修改任何外部状态,也不依赖外部状态。
  • 高阶函数:可以接受函数作为参数或者返回函数。
  • 惰性求值:表达式只在需要时才计算。

一些更接近纯函数式编程范式的编程语言有:

  • Haskell
  • Lisp (及其变种,如 Scheme 和 Clojure)
  • Erlang
  • F#

这些语言天生具有函数式编程的特性,但我们的 C++ 也在一步步地引入这些概念,让我们看看 C++ 是如何演变到今天的吧。

函数指针

首先当然是函数指针了,这是 C++ 中最原始的一种“函数式”手段。函数指针可以指向一个函数,然后通过这个指针调用这个函数。

#include <iostream>

void hello() {
    std::cout << "Hello, world!" << std::endl;
}

int main() {
    // 定义一个函数指针
    void (*funcPtr)() = hello;
    // 通过指针调用函数
    funcPtr();
    return 0;
}

这种方法虽然简单,但是它的局限性也很明显,比如只能指向某一种特定签名的函数,灵活性不足。

函数对象(Functor)

随后 C++ 中引入了函数对象(Functor)。通过重载 operator(),我们可以创建一个像函数一样调用的对象。

#include <iostream>

class HelloFunctor {
public:
    void operator()() const {
        std::cout << "Hello, world!" << std::endl;
    }
};

int main() {
    HelloFunctor hello;
    hello(); // 调用函数对象
    return 0;
}

函数对象比起函数指针更灵活,因为它可以保存状态和行为。不过,写起代码来多少有些繁琐。

std::functionstd::bind

到了 C++11,std::functionstd::bind 出现了。std::function 是一个通用的函数包装器,几乎可以保存任意的可调用对象。而 std::bind 则可以将函数和参数绑定起来生成新的函数。

#include <functional>
#include <iostream>

// 普通函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 使用 std::function 包装函数
    std::function<int(int, int)> func = add;
    std::cout << func(3, 4) << std::endl; // 输出 7

    // 使用 std::bind 绑定参数
    auto add_with_2 = std::bind(add, 2, std::placeholders::_1);
    std::cout << add_with_2(5) << std::endl; // 输出 7

    return 0;
}

这一步让 C++ 的函数处理能力更上了一个台阶。可以部分绑定参数,再把它们传递或者存储起来,方便多了。

Lambda 表达式

C++11 还引入了 lambda 表达式,让我们可以在代码的任何地方定义匿名函数,极大地提高了代码的简洁性和灵活性。

#include <iostream>

int main() {
    auto hello = []() {
        std::cout << "Hello, world!" << std::endl;
    };

    hello();

    int x = 42;
    auto printX = [x]() {
        std::cout << x << std::endl;
    };
    printX();

    return 0;
}

lambda 表达式不但让代码更加简洁,还可以捕获上下文中的变量,真是灵活至极。

C++20 的 Ranges

重点来了,C++20 引入了 Ranges 库,这真是一大进步,让 C++ 更接近现代的函数式编程风格。用 Ranges,我们可以像处理流一样处理序列,而且不需要手动写那些繁琐的循环。

基本用法

先来看一个简单的例子吧。

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

    auto result = numbers
                  | std::views::filter([](int n) { return n % 2 == 0; })
                  | std::views::transform([](int n) { return n * n; });

    for (auto n : result) {
        std::cout << n << ' ';
    }
    std::cout << std::endl;

    return 0;
}

这个例子里,我们用 std::views::filter 过滤掉了奇数,然后用 std::views::transform 把每个偶数平方。这种写法简洁优雅,而且更符合人类思维。

惰性求值

Ranges 还有一个厉害的地方,就是它是惰性求值的。意思是说,它不会在定义的时候马上计算,而是在真正需要结果的时候才计算,这样就避免了不必要的开销。

#include <iostream>
#include <ranges>

int main() {
    auto numbers = std::views::iota(1, 1000000)
                   | std::views::filter([](int n) { return n % 2 == 0; })
                   | std::views::transform([](int n) { return n * n; })
                   | std::views::take(5); // 仅获取前5个结果

    for (auto n : numbers) {
        std::cout << n << ' ';
    }
    std::cout << std::endl;

    return 0;
}

这个例子中,我们生成了从 1 到 1000000 的范围,但最后只取了前 5 个结果。因为是惰性求值的,整个过程非常高效。

4 16 36 64 100 

...Program finished with exit code 0
Press ENTER to exit console.
组合视图

Ranges 里的视图可以像管道一样组合起来,用 | 操作符,一看就知道数据是按什么顺序处理的。

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

    auto result = numbers
                  | std::views::filter([](int n) { return n % 2 == 0; })
                  | std::views::transform([](int n) { return n * n; })
                  | std::views::reverse;

    for (auto n : result) {
        std::cout << n << ' ';
    }
    std::cout << std::endl;

    return 0;
}

这个例子里,我们把前面的结果反转了一下,同样用的视图,代码依然非常清晰。

36 16 4 

...Program finished with exit code 0
Press ENTER to exit console.
生成和合并

还有一些其他很有用的视图,比如 std::views::iota 可以生成递增的序列,std::views::join 可以扁平化嵌套的范围。

#include <iostream>
#include <ranges>
#include <vector>

int main() {
    auto numbers = std::views::iota(1) | std::views::take(10); // 1到10

    for (auto n : numbers) {
        std::cout << n << ' ';
    }
    std::cout << std::endl;

    std::vector<std::vector<int>> nested = { {1, 2}, {3, 4}, {5, 6} };
    auto flat_view = nested | std::views::join;

    for (auto n : flat_view) {
        std::cout << n << ' ';
    }
    std::cout << std::endl;

    return 0;
}

第一个例子是生成从 1 开始的自然数序列,取前 10 个。第二个例子是把嵌套的 vector 扁平化,这种操作在实际中非常常见而且有用。

1 2 3 4 5 6 7 8 9 10 
1 2 3 4 5 6 


...Program finished with exit code 0
Press ENTER to exit console.

结语

there-are-two-kind-of-languages

通过这一路的演化,我们看到 C++ 引入的这些特性——从函数指针、函数对象、std::functionstd::bind、lambda 表达式,再到 C++20 的 Ranges,让我们可以越来越方便地写函数式风格的代码。

这些新特性不仅让我们的代码更简洁、更易读,更高效,还让我们更容易掌握函数式编程的理念。希望大家通过这篇文章,对这些特性有更深的理解,并把它们用到实际开发中,让你的代码更加优雅!


http://www.kler.cn/a/387751.html

相关文章:

  • 【Python TensorFlow】进阶指南(续篇一)
  • SpringBoot(七)使用mapper注解编写sql操作数据库
  • WebStorm 如何调试 Vue 项目
  • Rust:GUI 开源框架
  • LLM时代下Embedding模型如何重塑检索、增强生成
  • MarsCode算法题之二叉树供暖问题
  • linux devfreq 模块
  • flink 内存配置(五):网络缓存调优
  • video素材格式转换--mp4转webm(vue3+Nodejs)
  • 如何运营Github Org
  • Hunyuan-Large:推动AI技术进步的下一代语言模型
  • 刘艳兵-DBA027-在Oracle数据库,通常可以使用如下方法来得到目标SQL的执行计划,那么通过下列哪些方法得到的执行计划有可能是不准确的?
  • 鸿蒙next版开发:ArkTS组件自定义事件拦截详解
  • 腾讯混元3D模型Hunyuan3D-1.0部署与推理优化指南
  • 【PGCCC】Postgresql LWLock 原理
  • 孤岛的总面积(Dfs C#
  • IP系列之scan讨论
  • Java学习篇之JVM 调优
  • Hive 的数据类型
  • 分布式中常见的问题及其解决办法
  • BUG: scheduling while atomic
  • 软考中级 软件设计师 上午考试内容笔记(个人向)Part.2
  • 道品科技智慧农业中的自动气象检测站
  • vue3+ts+element-ui实现的可编辑table表格组件 插入单行多行 组件代码可直接使用
  • 当AI遇上时尚:未来的衣橱会由机器人来打理吗?
  • 133.鸿蒙基础01