70

Boost Signals库中,它们重载了 () 运算符。

这是 C++ 中的约定吗?对于回调等?

我在一位同事的代码中看到了这一点(他恰好是 Boost 的忠实粉丝)。在所有 Boost 的优点中,这只会让我感到困惑。

关于这种超载的原因有什么见解吗?

4

11 回答 11

164

重载 operator() 时的主要目标之一是创建一个仿函数。仿函数的行为就像一个函数,但它的优点是它是有状态的,这意味着它可以在调用之间保持数据反映其状态。

这是一个简单的仿函数示例:

struct Accumulator
{
    int counter = 0;
    int operator()(int i) { return counter += i; }
}
...
Accumulator acc;
cout << acc(10) << endl; //prints "10"
cout << acc(20) << endl; //prints "30"

函子在泛型编程中大量使用。许多 STL 算法都以非常通用的方式编写,因此您可以将自己的函数/函子插入到算法中。例如,算法 std::for_each 允许您对范围的每个元素应用操作。它可以像这样实现:

template <typename InputIterator, typename Functor>
void for_each(InputIterator first, InputIterator last, Functor f)
{
    while (first != last) f(*first++);
}

您会看到该算法非常通用,因为它是由函数参数化的。通过使用 operator(),此函数允许您使用仿函数或函数指针。这是一个显示两种可能性的示例:

void print(int i) { std::cout << i << std::endl; }
...    
std::vector<int> vec;
// Fill vec

// Using a functor
Accumulator acc;
std::for_each(vec.begin(), vec.end(), acc);
// acc.counter contains the sum of all elements of the vector

// Using a function pointer
std::for_each(vec.begin(), vec.end(), print); // prints all elements

关于您关于 operator() 重载的问题,是的,这是可能的。只要您遵守方法重载的基本规则(例如,仅在返回类型上重载是不可能的),您就可以完美地编写一个具有多个括号运算符的仿函数。

于 2008-11-25T14:29:56.567 回答
29

它允许一个类像一个函数一样工作。我在一个日志类中使用了它,调用应该是一个函数,但我想要这个类的额外好处。

所以是这样的:

logger.log("Log this message");

变成这样:

logger("Log this message");
于 2008-11-25T14:15:25.270 回答
6

许多人回答说它是一个函子,但没有说明函子比普通旧函数更好的一个重要原因。

答案是函子可以有状态。考虑一个求和函数——它需要保持一个运行总计。

class Sum
{
public:
    Sum() : m_total(0)
    {
    }
    void operator()(int value)
    {
        m_total += value;
    }
    int m_total;
};
于 2008-11-25T14:27:06.853 回答
4

您还可以查看C++ 常见问题解答的矩阵示例。这样做有很好的用途,但这当然取决于您要完成的工作。

于 2008-11-25T14:14:52.840 回答
4

仿函数不是函数,因此不能重载它。
您的同事是正确的,尽管 operator() 的重载用于创建“函子”——可以像函数一样调用的对象。结合期望“类函数”参数的模板,这可能非常强大,因为对象和函数之间的区别变得模糊。

正如其他发帖者所说:函子比普通函数有一个优势,因为它们可以有状态。此状态可用于单次迭代(例如计算容器中所有元素的总和)或多次迭代(例如查找多个容器中满足特定条件的所有元素)。

于 2008-11-25T14:15:20.827 回答
3

开始在你的代码中更频繁地使用std::for_each,std::find_if等,你就会明白为什么拥有重载 () 运算符的能力很方便。它还允许函子和任务有一个明确的调用方法,不会与派生类中其他方法的名称冲突。

于 2008-11-25T14:16:18.687 回答
3

在 C++ 中使用 operator() 形成函子函数式编程范式有关,这些范式通常使用类似的概念:闭包

于 2008-11-25T14:34:00.023 回答
2

函子基本上就像函数指针。它们通常旨在可复制(如函数指针)并以与函数指针相同的方式调用。主要的好处是,当您有一个使用模板仿函数的算法时,可以内联对 operator() 的函数调用。但是,函数指针仍然是有效的函子。

于 2008-11-25T14:18:32.287 回答
2

我可以看到的一个优势,但可以讨论,是 operator() 的签名在不同类型中看起来和行为相同。如果我们有一个类 Reporter,它有一个成员方法 report(..),然后是另一个类 Writer,它有一个成员方法 write(..),如果我们想同时使用这两个类,我们将不得不编写适配器某个其他系统的模板组件。它所关心的只是传递字符串或你有什么。如果不使用 operator() 重载或编写特殊类型的适配器,您将无法执行类似的操作

T t;
t.write("Hello world");

因为 T 要求有一个名为 write 的成员函数,它接受任何可隐式转换为 const char*(或者更确切地说是 const char[])的内容。此示例中的 Reporter 类没有,因此将 T(模板参数)作为 Reporter 将无法编译。

但是,据我所知,这适用于不同的类型

T t;
t("Hello world");

但是,它仍然明确要求类型 T 定义了这样的运算符,因此我们仍然对 T 有要求。就我个人而言,我认为函子并不太奇怪,因为它们很常用,但我宁愿看到其他机制这种行为。在像 C# 这样的语言中,您可以只传递一个委托。我对 C++ 中的成员函数指针不太熟悉,但我可以想象你也可以在那里实现相同的行为。

除了语法糖行为之外,我并没有真正看到运算符重载来执行此类任务的优势。

我敢肯定,有更多明知故犯的人比我有更好的理由,但我想我会提出我的意见,供大家分享。

于 2008-11-25T14:37:48.020 回答
1

另一位同事指出,这可能是一种将函子对象伪装成函数的方法。例如,这个:

my_functor();

是真的:

my_functor.operator()();

这是否意味着:

my_functor(int n, float f){ ... };

也可以用来重载它吗?

my_functor.operator()(int n, float f){ ... };
于 2008-11-25T14:10:59.697 回答
1

其他帖子很好地描述了 operator() 的工作原理以及它为什么有用。

我最近一直在使用一些非常广泛地使用 operator() 的代码。重载此运算符的一个缺点是某些 IDE 会因此成为不太有效的工具。在 Visual Studio 中,您通常可以右键单击方法调用以转到方法定义和/或声明。不幸的是,VS 不够聪明,无法索引 operator() 调用。尤其是在复杂的代码中,到处都被覆盖了 operator() 定义,很难弄清楚哪段代码在哪里执行。在某些情况下,我发现我必须运行代码并跟踪它才能找到实际运行的内容。

于 2008-11-25T15:13:16.997 回答