函子如何维护/存储对象的状态

How does the functor maintain/store state of a object

本文关键字:存储 对象 状态 维护 何维护      更新时间:2023-10-16

我是研究函子的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;
}

因为我看到了下面这样的代码,它在函子中使用引用变量来保持状态,所以在这里,我使用这个概念开发了一个简单的代码,如下所示:

示例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()通过引用/指针调用了函子?

感谢更多阅读材料。

当您调用std::generate时,它会获得自己的函子对象副本。然而,一旦进入该函数,它只是重复调用自己的对象的单个实例,因此状态在generate调用的内部保留,但在generate和调用者之间不保留

所以,把你的代码改为

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

并且之后CCD_ 4将仍然为零。在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共享相同的状态,并且该状态将在调用后可访问。

事实上,我们可以编写一个简单的ish包装器来实现自动化,所以您不需要为每个函子手工完成:

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));

意味着CCD_ 9将被更新到位。

正如Jerry Coffin所指出的,即使在调用内部也不能保证状态的保留,因此,即使不需要为调用方保留状态,也可以使用有状态函子来执行这样的操作。

当然,函子对象没有任何特殊的魔力,这与其他对象不同。但我看不出在您的例子中,函子在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具有memeber n时,状态会保存在其中。

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

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将是您的例子中函子的一个实例。

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

这取决于常见但不能保证的行为。具体来说,它依赖于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和赋值。

如果您有意反常,您仍然可以分配单个调用的所有值,并忽略其他调用的返回,但这确实清楚地表明,像上面第一个这样的实现是预期的(第二个实现的不符合)。

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

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

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