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

Whi*_*ear 5 c++ functor

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

根据一些学术脚本,函子可以保持与功能不同的状态。任何人都可以通过一些简单易懂的示例来详细说明吗?

我真的不明白为什么典型的常规功能不能做到这一点。对于这种新手问题,我真的感到很抱歉。

Jer*_*fin 3

作为一个非常简单的演示,让我们考虑快速排序。我们选择一个值(通常称为“主元”),并将输入集合分为比较小于主元的集合和比较大于或等于主元1的集合。

标准库已经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);
}
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

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

当然,从 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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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


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