C++函子优势-保持状态

C++ functor advantage - holding the state

本文关键字:状态 C++      更新时间:2023-10-16

我确实研究了函子的整个概念,不幸的是,我无法理解函子相对于典型函数的真正优势。

根据一些学术脚本,函子可以保持不同于函数的状态。有人能用一些简单易懂的例子来详细说明这一点吗?

我真的不明白为什么典型的正则函数不能做同样的事情。对于这种新手问题,我真的很抱歉。

作为一个非常琐碎的演示,让我们考虑一个快速排序。我们选择一个值(通常称为"pivot"),并将输入集合分为比pivot小的集合和比pivot1大的集合。

标准库已经有了std::partition,它可以自己进行分区——将集合分为满足指定条件的项和不满足指定条件条件的项。因此,要进行分区,我们只需要提供一个合适的谓词。

在这种情况下,我们需要一个简单的比较,比如:return x < pivot;。不过,每次传递枢轴值都会变得困难。std::partition只是从集合中传递一个值,并询问:"这是否通过了您的测试?"您无法告诉std::partition当前的枢轴值是什么,并让它在调用例程时将其传递给您的例程。当然,可以完成(例如,Windows中的许多枚举函数都是这样工作的),但它变得相当笨拙。

当我们调用std::partition时,我们已经选择了枢轴值。我们想要的是一种方法…将该值绑定到将传递给比较函数的参数之一。一种非常丑陋的方法是通过一个全局变量"传递"它:

int pivot;
bool pred(int x) { return x < pivot; }
void quick_sort(int *begin, int *end) { 
    if (end - begin < 2)
        return;
    pivot = choose_pivot(begin, end);
    int *pos = std::partition(begin, end, pred);
    quick_sort(begin, pos);
    quick_sort(pos, end);
}

我真的希望我不必指出,如果我们能帮助的话,我们宁愿不使用全局。避免这种情况的一个相当简单的方法是创建一个函数对象。当我们创建对象时,我们传递当前的pivot值,它将该值作为状态存储在对象中:

class pred { 
    int pivot;
public:
    pred(int pivot) : pivot(pivot) {}
    bool operator()(int x) { return x < pivot; }
};
void quick_sort(int *begin, int *end) { 
    if (end-begin < 2)
        return;
    int pivot = choose_pivot(begin, end);
    int *pos = std::partition(begin, end, pred(pivot));        
    quick_sort(begin, pos);
    quick_sort(pos, end);
}

这增加了一点点额外的代码,但作为交换,我们取消了一个全局交换——一个相当合理的交换。

当然,从C++11开始,我们还可以做得更好——该语言添加了"lambda表达式",可以为我们创建一个非常类似的类

void quick_sort(int *begin, int *end) { 
    if (end-begin < 2)
        return;
    int pivot = find_pivot(begin, end);
    auto pos = std::partition(begin, end, [pivot](int x) { return x < pivot; });
    quick_sort(begin, pos);
    quick_sort(pos, end);
}

这改变了我们用来指定类/创建函数对象的语法,但它仍然与前面的代码基本相同:编译器生成一个带有构造函数和operator()的类。我们用方括号括起来的值被传递给构造函数,(int x) { return x < pivot; }基本上成为该类2operator()的主体。

这使得代码更容易编写更容易阅读——但这并没有改变我们正在创建一个对象、"捕获"构造函数中的一些状态以及使用重载operator()进行比较的基本事实。

当然,比较恰好是我们在排序之类的事情上所需要的。它是lambda表达式和函数对象的常见用法,但我们当然不限于此。举个例子,让我们考虑"规范化"一个doubles集合。我们想找到最大的一个,然后用它除以集合中的每个值,这样每个项目都在0.0到1.0的范围内,但所有项目都保持着与以前相同的比率:

double largest = * std::max_element(begin, end);
std::for_each(begin, end, [largest](double d) { return d/largest; });

在这里,我们有几乎相同的模式:创建一个存储一些相关状态的函数对象,然后重复应用该函数对象的operator()来完成实际工作。


  1. 我们可以分为小于或等于和大于。或者我们可以创建三个组:小于、等于、大于。后者可以在存在许多重复的情况下提高效率,但目前我们真的不在乎
  2. 关于lambda表达式,我们需要了解的不仅仅是这个——我正在简化一些事情,而完全忽略其他我们目前不关心的事情