Предположим, я хочу удалить элементы уникальный из 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").
@ThomasSablik это тоже мое подозрение, но я бы хотел, чтобы кто-то логически доказал это. Cppreference великолепен, но это всего лишь вики. Пример, в котором он терпит неудачу, был бы звездным - с подробностями, касающимися компилятора и флагов компилятора.
@Fureeish Ваши входные данные, по моему мнению, имеют четыре (а не три) уникальных значения, которые необходимо удалить. 1, 3, 5 и 7.
@BoR моя ошибка, отредактировал вопрос.





Я добавил некоторые выводы:
#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 Нет, я удалил это
После того, как предикат вернет 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 стандарт говорит «не указано ... потому что алгоритмы могут исключать элементы, переходя от элементов, которые изначально находились в этом диапазоне». поэтому unspecified означает результат перемещения значения контейнера. использование int определенно небезопасно, потому что сохраняется предыдущее значение.
Вы неправильно поняли, как работает 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 Да, я переформулировал ответ, возможно, более правильно.
Я понимаю из ссылки, что вы можете получить неправильные результаты. Например. [1, 2, 1, 3] -> [1, 1, 3, 3] после удаления 2 (поскольку 3 является ходом, назначенным на позицию 2, а значение позиции 3 после назначения хода не определено). Теперь 3 не является уникальным и не будет удален на следующем шаге.