Разрешено ли алгоритмам стандартной библиотеки копировать аргументы предикатов?

Предположим, мы хотим удалить повторяющиеся значения из вектора ints. Обычное решение состоит в том, чтобы отсортировать вектор и стереть дубликаты с помощью идиомы «стереть-удалить». Но нам нужно поддерживать порядок элементов, которые не будут удалены, поэтому мы не можем сортировать. Таким образом, можно придумать такой предикат и использовать его с алгоритмом remove_if:

struct comp {
    std::set<int> s;
    comp() : s() {}
    bool operator()(int i)
    {
        return !(s.insert(i)).second;
    }
};

Но это сломается, если объект предиката будет по какой-то причине скопирован, так как мы получим две копии члена set. И действительно, реализация gcc remove_if делает именно это:

template<typename _ForwardIterator, typename _Predicate>
    _ForwardIterator
    remove_if(_ForwardIterator __first, _ForwardIterator __last,
          _Predicate __pred)
    {

      __first = _GLIBCXX_STD_A::find_if(__first, __last, __pred);

      if(__first == __last)                             // ^^^^^ here a copy is made
        return __first;
      _ForwardIterator __result = __first;
      ++__first;
      for(; __first != __last; ++__first)
        if(!bool(__pred(*__first)))
          {
            *__result = _GLIBCXX_MOVE(*__first);
            ++__result;
          }
      return __result;
    }

Обходной путь состоит в том, чтобы сделать set членом нашего функтора статическим:

struct comp {
    static set<int> s;
    comp() { s. clear(); }
    bool operator()(int i)
    {
        return !(s.insert(i)).second;
    }
};
set<int> comp::s;

Но остается вопрос:

Нужно ли нам убедиться, что возможная копия предикатного функтора не нарушит нашу логику? Есть ли в стандарте что-либо, предписывающее (или запрещающее) определенное поведение в отношении этой проблемы? Или это ошибка в реализации?


person jrok    schedule 17.06.2012    source источник


Ответы (3)


Да, в стандарте не указано, сколько раз будет копироваться предикат, а также не сказано, в каком порядке предикат будет применяться к элементам контейнера. По сути, предикаты должны действовать как чистые функции; они не должны иметь наблюдаемого состояния.1

Так что remove_if здесь не звучит как подходящий алгоритм. Хаки, такие как хранение set вне функтора, не решат проблему; вы все равно будете вызывать неопределенное поведение.

<ч> <под>1. Более подробное обсуждение см. в статье 39 («Сделайте предикаты чистыми функциями») статьи Скотта Мейерса Эффективный STL.

person Oliver Charlesworth    schedule 17.06.2012
comment
Существуют алгоритмы, для которых стандарт определяет порядок. как std::copy, нет? А что с Remark: stable в стандарте, разве это не требует сохранения порядка? - person jrok; 17.06.2012
comment
@jrok: он стабилен только в том смысле, что относительный порядок сохраняемых элементов не меняется, а не в том смысле, в котором этого желает ОП. (И copy не принимает предикат; copy_if принимает его, но не определяет порядок, в котором он применяется.) - person Oliver Charlesworth; 17.06.2012
comment
Тем временем я нашел соответствующий раздел о копировании предикатов в стандарте (25.1.10), но не могу найти ничего о предикатах, которым не разрешено сохранять состояние (не то чтобы я не верю вам или Скотту Мейерсу - книга в моем todo list точно :)) - person jrok; 17.06.2012
comment
Предикаты @jrok могут сохранять изменяемое состояние. Просто это может вызвать у вас проблемы, особенно если вы используете это состояние, чтобы определить, что возвращать во время вызова. Предикат должен возвращать одно и то же для одних и тех же входных данных, но стандарт не запрещает вам поступать иначе. - person juanchopanza; 17.06.2012
comment
Я вижу в этом логику. Спасибо всем (это не я ставил -1, просто кстати) - person jrok; 17.06.2012
comment
Обратите внимание, однако, что некоторые алгоритмы прямо подразумевают поддержку предикатов, которые сохраняют состояние. Например, for_each возвращает (копию) функтора, который вы передали, что полезно почти исключительно в том случае, если функтор поддерживает некоторое состояние, которое вы можете получить после применения его к коллекции. - person Jerry Coffin; 17.06.2012
comment
@JerryCoffin: Это правда. Но for_each не вызывает предикат своего аргумента. - person Oliver Charlesworth; 17.06.2012
comment
@OliCharlesworth: Верно, в стандарте этого нет, но довольно много других людей (честно говоря, это, вероятно, включает меня, по крайней мере, время от времени). - person Jerry Coffin; 17.06.2012
comment
Хотя порядок вызовов предиката для remove_copy_if не определен, он подразумевается другими ограничениями алгоритма: вывод сохраняется через OutputIterator (может быть только инкрементирован, без декрементов или произвольного доступа), результат stable (реализация не может начать тестирование с конца, учитывая, что она может писать только в прямом проходе) и не более чем N выполнений предиката, что означает, что алгоритм должен быть выполнен за один проход. Если я что-то не упустил, предыдущие требования заставляют выполнять алгоритм. - person David Rodríguez - dribeas; 17.06.2012

Нужно ли нам убедиться, что возможная копия предикатного функтора не нарушит нашу логику?

Да, вы должны предположить, что предикаты скопированы. В C++11 вы можете использовать std::ref или std::cref .

Альтернативой может быть изменение структуры comp таким образом, чтобы она брала set по ссылке:

struct comp {
    std::set<int>& s;
    comp(std::set<int> s) : s(s) {}
    bool operator()(int i)
    {
        return !(s.insert(i)).second;
    }
};

Примечание. Я не делаю никаких заявлений о том, будет ли это работать с remove_if, я просто решаю проблему скопированных предикатов, содержащих состояние, которое не следует копировать.

Редактировать Как указано в комментариях, подход в корне нарушен. Результат вызова предиката не должен зависеть от изменяемого состояния.

person juanchopanza    schedule 17.06.2012
comment
Я считаю, что результаты этого еще не определены; стандарт не устанавливает порядок, согласно которому remove_if должен применять предикат к элементам контейнера. - person Oliver Charlesworth; 17.06.2012
comment
@OliCharlesworth Я даже не рассматривал remove_if, просто имел дело с проблемой скопированных предикатов. Я уточню свой ответ. - person juanchopanza; 17.06.2012
comment
@OliCharlesworth, ты совершенно прав. Я даже не осознавал, что вызов предиката изменяет набор и возвращает результат на основе этой изменяющей операции. - person juanchopanza; 17.06.2012
comment
Он не работает для remove_if, другие алгоритмы указывают, в каком порядке они применяются к аргументам, что делает их подходящими для изменяемого предиката. - person Matthieu M.; 17.06.2012
comment
@OliCharlesworth: for_each ? - person Matthieu M.; 17.06.2012
comment
@MatthieuM.: Я думаю, что ключ с for_each заключается в том, что его аргумент (в стандарте) просто называется Function, в отличие, например, от remove_if, где он называется Predicate. - person Oliver Charlesworth; 17.06.2012

Да, им разрешено копировать аргументы неопределенное количество раз. Лучшим подходом, чем создание статического набора элементов, было бы создание набора вне функтора и передача его в качестве параметра конструктора. Внутренне сохранить указатель.

person David Rodríguez - dribeas    schedule 17.06.2012
comment
Я считаю, что результаты этого еще не определены; стандарт не устанавливает порядок, согласно которому remove_if должен применять предикат к элементам контейнера. - person Oliver Charlesworth; 17.06.2012
comment
@OliCharlesworth: правда, однако, поскольку в стандарте указано, что он применим к прямым итераторам, большинство реализаций фактически применяют его по порядку из соображений эффективности. Иногда я задаюсь вопросом, должно ли это явно требоваться в Стандарте, проблема стандарта, налагающего меньше гарантий, чем то, что обычно предлагается, заключается в том, что люди в конечном итоге полагаются (часто не осознавая этого) на эти гарантии, зависящие от реализации. - person Matthieu M.; 17.06.2012
comment
@OliCharlesworth: Помимо того, что уже упоминал Матье, еще более важно, чтобы результат сохранялся в OutputIterator (т. е. он не мог откатиться назад), а количество выполнений предиката точно соответствовало размеру контейнера. Это было бы невозможно реализовать каким-либо другим способом, кроме прямого цикла, проверяющего каждый элемент и принимающего решение о том, копировать его или нет. - person David Rodríguez - dribeas; 17.06.2012