Безопасно ли проходить контейнер во время выполнения std::remove_if?

Предположим, я хочу удалить элементы уникальный из std::vector (не избавляться от дубликатов, а оставить только те элементы, которые встречаются как минимум 2 раза), и я хочу добиться этого довольно неэффективным способом — вызывая std::count во время std::remove_ifing. Рассмотрим следующий код:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 6, 3, 6, 2, 7, 4, 4, 5, 6};

    auto to_remove = std::remove_if (vec.begin(), vec.end(), [&vec](int n) {
        return std::count(vec.begin(), vec.end(), n) == 1;
    });

    vec.erase(to_remove, vec.end());

    for (int i : vec) std::cout << i << ' ';
}

Из ссылка на std::remove_if мы знаем, что элементы, начинающиеся с to_remove, имеют значения неопределенные, но мне интересно, насколько они могут быть неуказанными на самом деле.

Чтобы немного объяснить мою озабоченность, мы видим, что элементы, которые должны быть удалены, — это 1, 3, 5 и 7 — единственные уникальные значения. std::remove_if переместит 1 в конец, но нет гарантии, что после указанной операции в конце будет значение 1. Может ли это быть (из-за того, что это значение равно неопределенные), что оно превратится в 3 и вызов std::count вернет количество (например) 2 для более позднего встречающегося значения 3?

По сути, мой вопрос: гарантированно ли это сработает, и под работай я подразумеваю неэффективное удаление уникальных элементов из std::vector?

Меня интересует как ответ языкового юриста (который может быть "стандарт говорит, что такая ситуация возможна, этого следует избегать"), так и практический ответ (который может быть "стандарт говорит, что такая ситуация возможна, но на самом деле это значение не может оказаться совершенно другим, например 3").

Я понимаю из ссылки, что вы можете получить неправильные результаты. Например. [1, 2, 1, 3] -> [1, 1, 3, 3] после удаления 2 (поскольку 3 является ходом, назначенным на позицию 2, а значение позиции 3 после назначения хода не определено). Теперь 3 не является уникальным и не будет удален на следующем шаге.

Thomas Sablik 16.07.2019 12:39

@ThomasSablik это тоже мое подозрение, но я бы хотел, чтобы кто-то логически доказал это. Cppreference великолепен, но это всего лишь вики. Пример, в котором он терпит неудачу, был бы звездным - с подробностями, касающимися компилятора и флагов компилятора.

Fureeish 16.07.2019 12:43

@Fureeish Ваши входные данные, по моему мнению, имеют четыре (а не три) уникальных значения, которые необходимо удалить. 1, 3, 5 и 7.

Bo R 16.07.2019 12:44

@BoR моя ошибка, отредактировал вопрос.

Fureeish 16.07.2019 12:45
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
4
172
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я добавил некоторые выводы:

#include <algorithm>
#include <iostream>
#include <vector>
#include <mutex>

int main() {
    std::vector<int> vec = {1, 2, 6, 3, 6, 2, 7, 4, 4, 5, 6};

    auto to_remove = std::remove_if (vec.begin(), vec.end(), [&vec](int n) {

        std::cout << "number " << n << ": ";
        for (auto i : vec) std::cout << i << ' ';
        auto c = std::count(vec.begin(), vec.end(), n);
        std::cout << ", count: " << c << std::endl;
        return c == 1;
    });

    vec.erase(to_remove, vec.end());

    for (int i : vec) std::cout << i << ' ';
}

и получил

number 1: 1 2 6 3 6 2 7 4 4 5 6 , count: 1
number 2: 1 2 6 3 6 2 7 4 4 5 6 , count: 2
number 6: 2 2 6 3 6 2 7 4 4 5 6 , count: 3
number 3: 2 6 6 3 6 2 7 4 4 5 6 , count: 1
number 6: 2 6 6 3 6 2 7 4 4 5 6 , count: 4
number 2: 2 6 6 3 6 2 7 4 4 5 6 , count: 2
number 7: 2 6 6 2 6 2 7 4 4 5 6 , count: 1
number 4: 2 6 6 2 6 2 7 4 4 5 6 , count: 2
number 4: 2 6 6 2 4 2 7 4 4 5 6 , count: 3
number 5: 2 6 6 2 4 4 7 4 4 5 6 , count: 1
number 6: 2 6 6 2 4 4 7 4 4 5 6 , count: 3
2 6 6 2 4 4 6 

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

Сначала число 4 считается дважды, а на следующем шаге число 4 считается трижды. Подсчеты неверны, и на них нельзя полагаться.

Любая причина для использования взаимных исключений здесь?

Fureeish 16.07.2019 12:58

@Fureeish Нет, я удалил это

Thomas Sablik 16.07.2019 13:00
Ответ принят как подходящий

После того, как предикат вернет true в первый раз, в диапазоне будет одно неопределенное значение. Это означает, что любые последующие вызовы предиката будут учитывать неопределенное значение. Таким образом, подсчет потенциально неверен, и вы можете либо оставить нетронутыми значения, которые вы собираетесь отбросить, либо отбросить значения, которые должны быть сохранены.

Вы можете изменить предикат, чтобы он подсчитывал, сколько раз он возвращал значение true, и соответствующим образом уменьшать диапазон. Например;

std::size_t count = 0;
auto to_remove = std::remove_if (vec.begin(), vec.end(), [&vec, &count](int n)
{
    bool once = (std::count(vec.begin(), vec.end() - count, n) == 1);
    if (once) ++count;
    return once;
 });

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

Я думаю, что вопрос сводится к тому, что именно означает неопределенные. Учитывая тот факт, что технически могу может быть чем угодно, я убежден, что это небезопасно. Спасибо.

Fureeish 16.07.2019 13:04

@Fureeish стандарт говорит «не указано ... потому что алгоритмы могут исключать элементы, переходя от элементов, которые изначально находились в этом диапазоне». поэтому unspecified означает результат перемещения значения контейнера. использование int определенно небезопасно, потому что сохраняется предыдущее значение.

local-ninja 16.07.2019 13:20

Вы неправильно поняли, как работает std::remove_if. Значения, подлежащие удалению, не обязательно сдвигаются в конец. Видеть:

Removing is done by shifting (by means of move assignment) the elements in the range in such a way that the elements that are not to be removed appear in the beginning of the range. cppreference

Это единственная гарантия состояния полигона. Насколько мне известно, не запрещено сдвигать все значения, и это все равно удовлетворит сложность. Поэтому возможно, что некоторые компиляторы сдвигают нежелательные значения в конец, но это будет просто дополнительная ненужная работа.

Пример возможной реализации удаления нечетных чисел из 1 2 3 4 8 5:

   v               - read position
   1 2 3 4 8 5     - X will denotes shifted from value = unspecified
   ^               - write position
     v          
   1 2 3 4 8 5     1 is odd, ++read
   ^
       v
   2 X 3 4 8 5     2 is even, *write=move(*read), ++both
     ^   
         v
   2 X 3 4 8 5     3 is odd, ++read
     ^
           v
   2 4 3 X 8 5     4 is even, *write=move(*read), ++both
       ^
             v
   2 4 8 X X 5     8 is even, *write=move(*read), ++both
         ^

   2 4 8 X X 5     5 is odd, ++read
         ^         - this points to the new end.

Так что, как правило, вы не можете полагаться на то, что count возвращает какие-либо значимые значения. Так как в случае, когда move==copy (как для ints), результирующий массив равен 2 4 8|4 8 5. Который имеет неправильный счет как для нечетных, так и для четных чисел. В случае std::unique_ptrX==nullptr и, следовательно, подсчет nullptr и удаленных значений может быть неправильным. Остальные оставшиеся значения нельзя оставлять в конце массива, так как не делалось копий.

Обратите внимание, что значения не являются неопределенными, поскольку вы не можете их знать. Это в точности результаты операций перемещения, которые могут оставить значение в неопределенном состоянии. Если бы он указывал состояние перемещенных переменных (как это делаетstd::unique_ptr), то они были бы известны. Например. если move==swap, то будет переставлен только диапазон.

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

Fureeish 16.07.2019 13:03

@Fureeish Да, я переформулировал ответ, возможно, более правильно.

Quimby 16.07.2019 13:07

Другие вопросы по теме