Как функтор поддерживает / сохраняет состояние объекта - PullRequest
4 голосов
/ 21 февраля 2012

Я C ++ Noob изучая функторы. У меня есть этот код, как показано ниже (NB - это не моя домашняя работа, я прошёл мимо этого!).

Он печатает 0 1 2 3 4 5 6 7 8 9 на консоли
который я не вижу, как он поддерживает состояние этого объекта (значение n), если функтор вызывается по значению, а не по ссылке / указателю

EDIT: Я подумал здесь (Пример 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 () вызывает функтор по ссылке / указателю?

Дополнительные материалы для чтения приветствуются.

Ответы [ 6 ]

2 голосов
/ 21 февраля 2012

Когда вы вызываете std::generate, он получает собственную копию объекта функтора. Оказавшись внутри этой функции, он просто вызывает свой отдельный экземпляр объекта несколько раз, поэтому состояние сохраняется внутри вызова generate, но не между generate и вызывающей стороной .

Итак, измените свой код на

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 будет обновлено на месте.

Как указывает Джерри Коффин, сохранение состояния даже внутри вызова не гарантируется, поэтому разумно делать что-то подобное с функторами с состоянием, даже если вам не нужно сохранять состояние для вызывающий.

0 голосов
/ 21 февраля 2012

Функция генерирования берет один экземпляр функтора и вызывает его снова и снова. Так что сохранение состояния такое же, как и в каждом нормальном классе. Я удалил (и упростил) это из заголовков моих компиляторов (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 будет одним экземпляром вашего функтора в ваших примерах.

Обратите внимание, что мой компилятор оптимизировал ваш первый пример, поэтому создание копии не производилось. При использовании именованной переменной вместо этого имела место конструкция копирования.

0 голосов
/ 21 февраля 2012

Это зависит от поведения, которое распространено, но не гарантировано.В частности, он рассчитывает на то, что 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 и присваивает возвращаемое значение gen через все итераторы в диапазоне [first, last) или [first, first + n).

Учитывая изложенный способ, вы можете утверждать, что в основном говорится, что вы вызываете gen только один раз, а затем присваиваете это возвращаемое значение всем итераторам в диапазоне, а не повторно вызываете его для каждогоитератор в диапазоне.Например, он говорит о «возвращаемом значении», подразумевая, что существует только одно значение, а не отдельное возвращаемое значение для каждого итератора в диапазоне.

Редактировать: Перечитывание, я думаю, что стандарт действительно обеспечиваетубедительный признак того, что первый предназначен, хотя.Читая чуть ниже, мы получим:

Сложность: точно последний - первый (или n) вызовы gen и назначений.

Если вы были намеренно извращены,Вы все еще можете назначить все значения из одного вызова и игнорировать результаты других вызовов, но это действительно дает понять, что реализация, подобная первой, приведенной выше, - это то, что задумано (а вторая делает не * 1035)* соответствовать как есть).

0 голосов
/ 21 февраля 2012

Функторы точно такие же, как и любые другие объекты - если их член определен как ссылка, они сохраняют его по ссылке, если он определяется как значение, они сохраняют значение.Ваш первый пример работает, потому что std :: generate получает свой параметр functor по значению, а не по ссылке, и, таким образом, работает с копией временного объекта, который вы создаете в выражении с g().

0 голосов
/ 21 февраля 2012

Пример 1 работает, потому что у объекта функтора (v1) есть переменная-член (n), которая увеличивается каждый раз при вызове объекта.

Пример 2 отличается только тем, что объект функтора (v1)обновляет ссылку на переменную, которая находится за пределами объекта.

Пример 1 - лучший объектно-ориентированный проект, потому что каждый создаваемый вами объект класса g будет заботиться о своем собственном подсчете, тогда как в примере 2 онвызывающий обязан убедиться, что счетчик не является общим и имеет, по крайней мере, такой же срок службы, что и объект-функтор.

0 голосов
/ 21 февраля 2012

Конечно, у объектов-функторов нет особой магии, которая отличает их от других объектов. Но я не вижу, какой функтор состояния должен сохраняться при копировании в вашем примере.

Рассмотрим первый: Возможна 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, в нем сохраняется состояние.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...