【问题标题】:How does the functor maintain/store state of a object仿函数如何维护/存储对象的状态
【发布时间】:2012-03-11 22:02:57
【问题描述】:

我是学习函子的 C++ 菜鸟。我有如下代码(注意 - 这不是我的作业,我已经过去了!)。

它确实在控制台上打印 0 1 2 3 4 5 6 7 8 9
如果函子是按值而不是按引用/指针调用的,我看不出它如何保持这个对象的状态(n 的值)

编辑: 我想在这里(示例 1),因为函子是由 Value 调用的,并且构造函数每次都将 n 初始化为零。所以它应该在开始时总是为零,然后它应该递增到 1 并返回 1。它是如何打印 0 1 2 3 4 5 6 7 8 9

示例 1]

class g
{
public:
    g():n(0){}
    int operator()() { return n++; }
    int n;
};

;

int main()
{
    int a[10];
    g v1;
    std::generate(a, a+10, g());//This passes a functor to generate 

    //EDIT - this will print 0 1 2 3 4 5 6 7 8 9**
    std::copy(a, a+10, std::ostream_iterator<int>(std::cout, " "));

    getchar();
    return 0;
}

因为我已经看到下面的代码使用仿函数内的引用变量来保留状态,here 并使用该概念开发了一个简单的代码,如下所示:

示例 2]

class CountingFunctor
{
public:
    CountingFunctor() : _counter(0) {}
    int getCounter(void) const {return(_counter);}
    void operator () (Contained item) {if(item.getShouldBeCounted()) _counter++;}
private:
    int _counter;
};
#endif

//this class uses references to maintain state in the functor
class CountingFunctor
{
public:
    CountingFunctor(int &elem) : _counter(elem) {_counter=0;}
    int getCounter(void) const {return(_counter);}
    void operator () (Contained item) {if(item.getShouldBeCounted()) _counter++;}
private:
    int &_counter;
};

int main()
{
    vector<Contained> Container(10);
    Container[3].setShouldBeCounted(false);
    Container[9].setShouldBeCounted(false);
    int elem;
    CountingFunctor CountAllWhoShouldBe(elem);
    std::for_each(Container.begin(), Container.end(), CountAllWhoShouldBe);
    std::cout << CountAllWhoShouldBe.getCounter() << " items should be counted." << std::endl;

    getchar();
}

问题是

所以函子自己维护其对象的状态,即不需要任何引用变量,如示例 2 所示

或者示例 1 中的代码工作正常,因为 std::generate() 通过引用/指针调用函子?

欢迎进一步阅读材料。

【问题讨论】:

    标签: c++ function object


    【解决方案1】:

    当您调用std::generate 时,它会获得自己的仿函数对象副本。但是,一旦进入该函数,它只是重复调用它自己的对象的单个实例,因此状态保留在 inside generate 调用中,但 notgenerate 和调用者。

    所以,把你的代码改成

    g v1;
    std::generate(a, a+10, v1);
    

    之后v1.n 仍然为零。在 generate 内部,它在其本地副本(例如 v2)上运行,该副本确实增加了,但无法告诉 v1。

    现在,如果您想将 v2 的状态传达给 v1,这时您需要在仿函数中使用引用,因此 v1 和 v2 共享调用中发生变异的任何状态。


    我们可以扩展调用以更清楚地显示这一点:

    g v1;
    std::generate(a, a+10, v1);
    // -> generate(begin=a, end=a+10, v2=g(v1))
    {
        while (begin != end)
            *begin = v2();
    }
    // v2 just went out of scope, and took the accumulated state with it!
    // v1 in the caller's scope remains unchanged
    

    现在应该很明显,如果v1 不是一个被深度复制并在内部保持其状态的值对象,而是保持对共享状态的引用并被浅复制,那么v2 将共享相同的状态为v1,并且在调用后可以访问该状态。

    事实上,我们可以编写一个简单的包装器来自动执行此操作,因此您无需为每个仿函数手动完成:

    template <typename OriginalFunctor, typename RType>
    class StatefulFunctor
    {
        OriginalFunctor &fun;
    
    public:
        StatefulFunctor() = delete;
        StatefulFunctor(OriginalFunctor &orig) : fun(orig) {}
        StatefulFunctor(StatefulFunctor const &other) : fun(other.fun) {}
        StatefulFunctor(StatefulFunctor &&other) : fun(other.fun) {}
    
        template <typename... Args>
        RType operator() (Args&&... args)
        {
            return fun(std::forward<Args>(args)...);
        }
    };
    
    template <typename RT, typename OF>
    StatefulFunctor<OF, RT> stateful(OF &fun)
    {
        return StatefulFunctor<OF, RT>(fun);
    }
    

    现在把原来的代码改成:

    g v1;
    std::generate(a, a+10, stateful<int>(v1));
    

    表示v1.i 将就地更新。

    正如 Jerry Coffin 指出的那样,即使在调用中内部也不能保证状态的保存,因此即使您不需要保存状态的函数,使用有状态函子做这样的事情也是明智的。调用者。

    【讨论】:

    • 谢谢。这对我来说是新的——“generate() 获取它自己的仿函数对象副本,并且对于重复调用,对于容器中的每个元素,它调用同一个对象..
    • 是的 - 我什至不确定标准 保证 它会在 generate 或任何其他调用中使用函子的单个副本,但当然是常规函数是无国籍的,所以没关系
    【解决方案2】:

    当然,函子对象没有任何特殊的魔法,它们与其他对象不同。但是在您的示例中,我看不到在 copting 时应该保存什么状态函子。

    考虑第一个: 可能的generate实现是

    template <typename Iterator, typename Functor>
    void generate(Iterator begin, Iterator end, Functor f)
    {
        for (Iterator it  = begin; it != end; ++it) {
            *it = f();
        }
    }
    

    在这个例子中,仿函数只在函数入口处复制了一次,而代码处理的是局部变量f,并且它不执行任何复制。

    虽然您的functor 拥有成员n,但状态会保存在其中。

    【讨论】:

      【解决方案3】:

      Functors 与任何其他对象完全一样 - 如果它们的成员被定义为引用,它们通过引用存储它,如果它被定义为一个值,它们存储一个值。您的第一个示例有效,因为 std::generate 通过值而不是通过引用获取其函子参数,因此对您在 g() 表达式中创建的临时副本进行操作。

      【讨论】:

      • 如果everycall是仿函数的本地副本,那么它的每次构造函数都将其初始化为零。我错过了什么吗?
      • 仿函数调用实际上是一个方法调用——它不会创建对象的副本,在该副本上调用方法(您的仿函数)。创建副本的原因是将一个对象(在本例中为您的仿函数)作为值参数传递给函数。
      • 想想一个按值获取 int 的函数是如何工作的:void foo(int i) { ... - 该函数创建自己的私有 int,如果它修改 int(例如,++i),只有它的私有副本受到影响。副本的生命周期是函数调用 - 它不会在您每次执行 ++i 时创建一个新副本
      【解决方案4】:

      generate 函数获取函子的一个实例并一遍又一遍地调用它。所以状态保存与每个普通类相同。我从我的编译器(gcc 4.5)头文件中剥离(并简化)了这个:

      template<typename _ForwardIterator, typename _Generator>
      void
      generate(_ForwardIterator __first, _ForwardIterator __last,
               _Generator __gen)
      {
        // concept requirements -- ommitted for easy reading
        for (; __first != __last; ++__first)
          *__first = __gen();
      }
      

      如您所见,__gen 将是您的示例中仿函数的一个实例。

      请注意,我的编译器优化了您的第一个示例,因此没有进行复制构造。当使用命名变量时,会发生复制构造。

      【讨论】:

        【解决方案5】:

        这取决于常见的行为,但不能保证。具体来说,它指望generate 将为每个分配的值重新调用函数对象,如下所示:

        template <class FwdIt, class Generator>
        void generate(FwdIt first, FwdIt last, Generator gen) {
            while (first != last) {
                *first = gen();
                ++first;
            }
        }
        

        我相信标准允许这样做,但我很确定它不能保证这一点。至少根据我对标准的阅读,它完全可以按照这个一般顺序做某事:

        template <class FwdIt, class Generator>
        void generate(FwdIt first, FwdIt last, Generator gen) {
            decltype(*first) holder = gen();
            while (first != last) {
                *first = holder;
                ++first;
            }
        }
        

        在这种情况下,范围内的每个项目都将被分配相同的值。也就是说,这似乎是实现generate 的一种相当不寻常的方式。我很确定这是允许的,但没有看到 太多 这样做的理由。

        同时,我应该指出,这样做有一些细微的理由。首先是效率:存储价值可能比重新创建 N 次更便宜。

        第二个将基于对标准(§25.2.6/1)中描述的仔细(迂腐)阅读:

        效果:调用函数对象gen并通过[first, last)[first, first + n)范围内的所有迭代器分配gen的返回值。

        考虑到措辞的方式,您可能会争辩说,它基本上是说您只调用一次gen,然后将该返回值分配给范围内的所有迭代器,而不是为范围内的每个迭代器重新调用它.例如,它谈到“返回值”,暗示只有一个值,而不是范围内的每个迭代器都有一个单独的返回值。

        编辑:重新阅读,我认为该标准确实提供了一个强有力的迹象,表明第一个是有意的。再往下读一点,我们得到:

        复杂性:恰好最后 - 第一次(或 n 次)调用 gen 和 assignments。

        如果你是故意反常的,你仍然可以从一次调用中分配所有值,并忽略其他调用的返回,但这确实很清楚,像上面第一个这样的实现是预期的(第二个按原样符合)。

        【讨论】:

        • generator 不会打印 0 1 2 3 4 5 6 7 8 9
        • 一个相关问题...编译器如何翻译“std::generate(a, a+10, g());”行中的 g()在“decltype(*first) holder = gen();”行中调用构造函数 g() 和 g() 调用到重载的 () 运算符?
        • @Lol4t0:第一个会(假设您添加了代码来进行打印,无论如何)。第二个不会 - 我很确定两者都符合标准的要求。
        • @Unni g 是一个类型,所以 g() 创建一个临时的并调用它的构造函数。 geng 类型的实例,因此 gen() 在该实例上调用 g 的函数调用运算符
        • 遗憾的是,我猜递归实现也应该符合要求,并且每次调用 operator() 时都可以使用一个新的仿函数实例
        【解决方案6】:

        示例 1 有效,因为函子对象 (v1) 有一个成员变量 (n),该成员变量在每次调用对象时递增。

        示例 2 的不同之处在于函子对象 (v1) 仅更新对位于对象外部的变量的引用。

        示例 1 是一种更好的面向对象设计,因为您创建的 g 类的每个对象都将负责自己的计数,而在示例 2 中,调用者有责任确保计数器不被共享并且具有至少与仿函数对象的寿命相同。

        【讨论】:

        • 在示例 1 中,函子由 Value 调用,并且构造函数每次都将 n 初始化为零。所以它应该在开始时总是为零,然后它应该递增到 1 并返回 1。它是如何打印 0 1 2 3 4 5 6 7 8 9
        • 函子不是“按值调用”——没有这样的东西。它按值传递std::generate 函数,但这只发生一次- 当您调用generate 时。 generate 函数本身多次调用函子对象的operator() - 但这不会调用构造函数代码,它就像对现有对象的普通成员函数调用一样。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-06-27
        • 1970-01-01
        • 2016-05-11
        • 2010-11-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多