【问题标题】:Why override operator()?为什么要覆盖 operator()?
【发布时间】:2010-09-23 23:10:03
【问题描述】:

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

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

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

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

【问题讨论】:

标签: c++ boost operator-overloading functor function-call-operator


【解决方案1】:

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

【讨论】:

  • 我认为这个答案的很大一部分是 STL for_each 的语法。通过使用 operator() 作为函子的操作部分,它将与 STL 一起工作。
  • 看来如果 STL 被实现为 do(){ ... } 而不是 operator()(){ ... } do 将被使用。
  • 仿函数相对于函数的另一个(通常是次要的)优势是它们可以被简单地内联。不涉及指针间接,只是在类上调用(非虚拟)成员函数,因此编译器可以确定调用哪个函数,并将其内联。
  • 删除了我关于为什么特别选择 operator() 的评论,因为您将其编辑到您的帖子中:)
  • @MarkRansom 但是,如果您改为传递函子,例如上面定义的Accumulator,那么for_each 将被实例化为Accumulator,并且在其主体中调用的函数是@ 987654327@。因此编译器知道将调用哪个函数,而不管在运行时传入的实际值如何。这允许它简单地内联调用
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

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

    my_functor();
    

    真的是:

    my_functor.operator()();
    

    这是否意味着:

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

    也可以用来重载这个吗?

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

    【讨论】:

    • 您的最后一行根本不是运算符重载。它必须是:“.operator()(int n, float f)”,当你第一次看到它时,它看起来很混乱。你可以像其他函数一样重载这个“函数调用运算符”,但是你不能用你指定的非运算符重载来重载它。
    • 你的第二行是错误的,它实际上是“my_functor.operator()();”。 my_functor.operator() 是方法引用,而第二组 () 表示调用。
    【解决方案4】:

    我可以看到的一个优点,但是可以讨论,是 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++ 中的成员函数指针不太熟悉,但我可以想象你也可以在那里实现相同的行为。

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

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

    【讨论】:

    • 使用 operator() 的好处是你的模板参数既可以是函数指针也可以是仿函数。
    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

      在 C++ 中使用 operator() 形成 functorsfunctional programming 范例有关,后者通常使用类似的概念:closures

      【讨论】:

        【解决方案7】:

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

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

        class Sum
        {
        public:
            Sum() : m_total(0)
            {
            }
            void operator()(int value)
            {
                m_total += value;
            }
            int m_total;
        };
        

        【讨论】:

        • 这并不能解释为什么需要隐藏它是一个对象并将它伪装成一个函数的事实。
        • 杰夫五世:方便。这意味着无论我们是调用函子还是函数指针,都可以使用相同的语法进行调用。例如,如果您查看 std::for_each,它可以与仿函数或函数指针一起使用,因为在这两种情况下,调用的语法是相同的。
        【解决方案8】:

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

        【讨论】:

          【解决方案9】:

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

          【讨论】:

            【解决方案10】:

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

            所以是这样的:

            logger.log("Log this message");
            

            变成这样:

            logger("Log this message");
            

            【讨论】:

              【解决方案11】:

              您也可以查看C++ faq's Matrix example。这样做有很好的用途,但这当然取决于您要完成的工作。

              【讨论】:

                猜你喜欢
                • 2012-01-05
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2018-07-30
                相关资源
                最近更新 更多