Удаление необработанных указателей из std::vector

У меня есть следующий шаблон:

  1. У меня есть std::vector, содержащий необработанные указатели на объекты (я знаю, что необработанные указатели - это "зло", но это устаревшее программное обеспечение, которое необходимо поддерживать).
  2. Теперь для каждого элемента в векторе мне нужно сделать тест, и если тест положительный, сделайте что-нибудь с указателем, удалите его, а затем удалите из вектора:

Псевдокод:

for each pointer in vector
{
  if (SomeTest(pointer))
  {
     DoSomething(pointer)
     delete pointer
     remove pointer from vector
  }
}

Я не могу придумать хороший чистый код для этого.

Эта ссылка предлагает разные подходы, но все они кажутся мне более или менее громоздкими.

Громоздкое решение, которое я использую сейчас:

for(auto &  p : v)
{
   if (SomeTest(p))
   {
       DoSomething(p);
       delete p;
       p = nullptr;
   }
}

v.erase(std::remove(v.begin(), v.end(), nullptr), v.end());

извините, придирка: необработанные указатели на объекты, принадлежащие кому-то другому, довольно безвредны. Только обладание необработанными указателями является злом

463035818_is_not_a_number 11.02.2019 15:04

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

Kenny Ostrom 11.02.2019 15:04

Что конкретно не так с erase_if?

Useless 11.02.2019 15:07
Я знаю, что необработанные указатели - это "зло", но это устаревшее программное обеспечение, которое необходимо поддерживать. Какую версию C++ вам разрешено использовать? Можете ли вы частично обновить его, чтобы использовать правильную механику?
NathanOliver 11.02.2019 15:12

@user463035818 user463035818 Излишне говорить, что deleteing необработанные указатели имеет смысл только в крайнем случае владения необработанными указателями. Утверждение в вопросе явно не приписывает владение указателям, поэтому вы технически правы, чтобы придираться.

Max Langhof 11.02.2019 15:13

@MaxLanghof, если вектор отвечает за них delete, то я бы сказал, что вектор ими владеет. Во всяком случае, я специально имел в виду «я знаю, что необработанные указатели — это зло», и я немного придирчив, когда что-то получает атрибут «зло». Слишком легко это может привести к непониманию

463035818_is_not_a_number 11.02.2019 15:15

@MaxLanghof в моем случае вектор является, владеющий указателями.

Jabberwocky 11.02.2019 15:16

Обновление @NathanOliver для использования правильной механики было бы довольно долгим, но да, это определенно было бы лучшим решением. Я могу использовать С++17.

Jabberwocky 11.02.2019 15:17

@Jabberwocky Это была моя точка зрения. Вы не написали это черным по белому рядом с частью «злые указатели», поэтому технически придирка этого парня оправдана, но я хотел указать, что очевидно, что их придирка здесь не применима.

Max Langhof 11.02.2019 15:17
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
20
9
2 646
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Ответ принят как подходящий

Как часто ответ: знай свои <algorithm> (и это хорошее напоминание самому себе) ;)

std::partition - это то, что вам нужно: std::partition(begin, end, p) "перемещает" элементы диапазона [begin, end), не надо которых удовлетворяет предикату p в конце диапазона; затем вы можете рассматривать их как партию.

auto const to_be_removed = std::partition(begin(v), end(v), [](auto p){ /* predicate */ });
std::for_each(to_be_removed, end(v), [](auto p) {
    /* crunch */
    delete p;
});
v.erase(to_be_removed, end(v));

Полная программа

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

int main()
{
    std::vector v = { new int{0}, new int{1}, new int{2} };

    // let's delete all even values
    auto const to_be_removed = std::partition(begin(v), end(v), [](auto p){ return *p % 2 != 0; });
    std::for_each(to_be_removed, end(v), [](auto p) {
        std::cout << "Deleting value " << *p << "...\n";
        delete p;
    });
    v.erase(to_be_removed, end(v));
}

Живая демонстрация

Чтобы пойти дальше

У этой реализации есть два основных недостатка: порядок из вектора нестабилен (1), его можно было бы преобразовать в повторно используемую функцию (2).

  • (1) решается std::stable_partition.
  • (2) не так сложно:
template<class InputIt, class UnaryPredicate, class UnaryDeleter>
InputIt delete_if (InputIt begin, InputIt end, UnaryPredicate p, UnaryDeleter d)
{
    auto const to_be_removed = std::stable_partition(begin, end, std::not_fn(p));
    std::for_each(to_be_removed, end, [d](auto p) { d(p) ; delete p; });
    return to_be_removed;
}

template<class Container, class UnaryPredicate, class UnaryDeleter>
auto delete_if (Container& c, UnaryPredicate p, UnaryDeleter d)
{
    using std::begin, std::end;
    return c.erase(delete_if (begin(c), end(c), p, d), end(c));
}

использование:

delete_if (v, SomeTest, DoSomething);

Живая демонстрация

Кроме того, delete p должен обрабатываться UnaryDeleter, иначе его можно использовать только для динамических указателей.

user202729 11.02.2019 17:13

Я не согласен с этим пунктом; я полагаю, что мы решаем конкретную проблему OP: «для каждого элемента в векторе мне нужно сделать тест, и если тест положительный, сделайте что-нибудь с [необработанным] указателем, удалите его, а затем удалите из вектора». У них нет причин не помещать оператор delete в функцию delete_if.

YSC 11.02.2019 17:24

Предположим, у вас есть вектор указателя int. Вот мое решение:

vector<int*> vpi;

for (vector<int*>::iterator it = vpi.begin(); it != vpi.end(); )
{
    if (SomeTest(*it))
    {
        DoSomething(*it)
        int* old = *it;
        it = vpi.erase(it);
        delete old;
    } else
    {
       it++;
    }
}

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

463035818_is_not_a_number 11.02.2019 15:10

@LocTran Я обычно делаю it = vector.erase(it) .. не так хорошо, как твой старый, новый :)

Samer Tufail 11.02.2019 15:15

Зачем использовать old? DoSomething(*it) delete *it; it = vpi.erase(it); делает то же самое.

NathanOliver 11.02.2019 15:16

@NathanOliver приятно знать, что кто-то еще делает то же самое

Samer Tufail 11.02.2019 15:17

если вы удалите *it, исходный указатель потеряется или нет (не определено). Лучший способ сохранить старый указатель

Loc Tran 11.02.2019 15:17

Это решение O (N ^ 2) (подумайте об элементах, которые нужно переместить, когда вы стираете в середине). Вместо этого используйте std::partition, а затем один erase.

rustyx 11.02.2019 15:19

@LocTran Что ты имеешь в виду под исходный указатель потеряет или нет (не определено)? Если вы уберете элемент из вектора сразу после того, как проблем не возникнет. Наличие old не делает операцию безопаснее. Это только загромождает код.

NathanOliver 11.02.2019 15:19

Не сильно отличается от того, что вы делаете. Что это дает в миксе? Многопоточность не безопаснее, и это единственная причина, по которой у вас может быть это временно.

Matthieu Brucher 11.02.2019 15:19

@rustyx, я не думаю, что это решение O (N ^ 2). Оно включено). Почему О(N^2)? могли бы вы объяснить?

Loc Tran 11.02.2019 15:21

Рассмотрим вектор из 1000 элементов, из которых нужно удалить первые 100. Сколько раз оставшийся элемент нужно будет переместить внутри вектора (около 95000).

rustyx 11.02.2019 15:27

@rustyx хорошо знать std::partition. впервые вижу эту функцию

Loc Tran 11.02.2019 17:25

Некоторый подход, который я бы использовал:

for (auto i = vector.begin(); i != vector.end(); ++i) {
  if (SomeTest(*i)) {
    DoSomething(*i);
    delete *i;
    *i = nullptr;
  }
}
vector.erase(std::remove(vector.begin(), vector.end(), nullptr), vector.end());

Это более или менее то, что я делаю сейчас

Jabberwocky 11.02.2019 15:39

@UmNyobe Что плохого в использовании nullptr в качестве магического значения?

Galik 11.02.2019 15:43

@Galik, возможно, вектор действительно содержит nullptr, и, возможно, SomeTest возвращает false для nullptr.

463035818_is_not_a_number 11.02.2019 15:44

Самое простое решение — начиная со статьи по ссылке — взять функцию erase_if.

template <typename Container, typename Pred>
void erase_if (Container &c, Pred p)
{
    c.erase(std::remove_if (std::begin(c), std::end(c), p), std::end(c));
}

и просто позвоните с

erase_if (v, [](T *pointer)
         {
           if (SomeTest(pointer))
           {
              DoSomething(pointer);
              delete pointer;
              return true; //remove pointer from vector
           }
           return false;
         });

Очевидно, вы можете разделить свой предикат на две части, если хотите отделить часть SomeTest/DoSomething от части delete:

template <typename Container, typename Pred>
void delete_if (Container &c, Pred p)
{
    auto e = std::remove_if (std::begin(c), std::end(c),
        [&p](Container::value_type *pointer)
        {
          if (p(pointer)) {
            delete pointer;
            return true;
          }
          return false;
        });
    c.erase(e, std::end(c));
}

Поскольку вы не сказали Зачем, что вам не нравится erase_if, который вы сами связали, я не могу предположить, имеет ли он ту же проблему.

С этим подходом есть проблема: std::erase_if (...) эквивалентен v.erase(std::remove_if (...)), в котором явно указывает соответствует предикату не должен изменять элемент.

You 11.02.2019 16:37

@You Технически это не изменяет элемент, но я думаю, смысл в том, что p должен быть идемпотентным (вызываемым много раз без вреда), и это, конечно, не так в этом случае, поскольку следующий вызов удаленного указателя, вероятно, будет иметь УБ.

Arne Vogel 11.02.2019 16:44

Честная оценка. (Я полагаю, что точная формулировка такова: «[предикат] не должен применять какую-либо неконстантную функцию через разыменованный итератор», чего deleteing технически не делает, я думаю, поскольку это нормально для delete как постоянный указатель.)

You 11.02.2019 16:49

@ArneVogel Стандарт гарантирует, что предикат будет вызываться только один раз для каждого элемента.

Galik 11.02.2019 18:12

Я предполагаю, что вы выводите это из (алг.удалить/5) - ИМО, это все еще немного технический вопрос. Если бы я делал обзор кода, я бы предложил переключиться на partition/stable_partition или написать осмысленный комментарий, почему это правильно.

Arne Vogel 11.02.2019 21:44

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

std::vector<int*> v;

v.erase(std::remove_if (std::begin(v), std::end(v), [](int* p){

    // do your test and do not remove on failure
    if (!SomeTest(p))
        return false; // keep this one

    DoSomething(p);

    // Now we remove but be sure to delete here, before the
    // element is moved (and therefore invalidated)

    delete p;

    return true; // signal for removal

}), std::end(v));

Примечания: Почему это безопасно.

Удаление указателя изменяет не сам указатель, а объект, на который он указывает. Это означает, что при таком подходе никакие элементы не изменяются.

Стандарт C++17 28.6.8 5 гарантирует, что предикат будет вызываться только один раз для каждого элемента.

Отвечая на ваш комментарий, да: это хорошая идея для этого конкретного случая. (+1)

YSC 11.02.2019 15:53

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

auto badIt = std::stable_partition(std::beging(v), std::end(v), SomeTestInverse);
std::for_each(badIt, std::end(v), [](auto e){ DoSomething(e); delete e;});
v.erase(badIt,std::end(v));

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

Будь проще. ЯГНИ. Нет причин решать более общую и сложную версию проблемы, если она вам понадобится (подсказка: вам не понадобится) или искать непонятные методы STL (ну, если вы не желание).

size_t target = 0;
for (size_t idx = 0; idx < v.size(); idx++) {
    if (should_delete(v[idx]))
        delete v[idx];
    else
        v[target++] = v[idx];
}
v.resize(target);
<algorithm> не является связкой "непонятные методы STL". Изучите их, используйте их. Они создают легко читаемый, простой в обслуживании и легко оптимизируемый код.
YSC 12.02.2019 08:14

Конечно, «простота» — это в глазах смотрящего, но я видел слишком много кода с несколькими уровнями шаблонного наследования, который можно было бы написать с помощью простого цикла for или оператора switch, только если бы программист сопротивлялся желанию обобщить все до максимума. Если кому-то std::for_each с лямбдами покажется проще, чем for, то это на их вкус: но сомневаюсь, что будет быстрее.

jick 12.02.2019 19:19
"Конечно, "легко" в глазах смотрящего" Нет, это вообще не так. Если в сообществе C++ существует идиома, все, кроме этой идиомы, нелегко читать и увеличивает wtf/строку вашей кодовой базы. Про оптимизацию ничего нельзя сказать без тестирования, но в целом идиоматический код и код, опирающийся на стандартную библиотеку, лучше понимается компилятором и лучше оптимизируется.
YSC 12.02.2019 19:25

...Легко ли нет в глазах смотрящего? Я рад, что вы согласны с тем, что петля for объективно лучше! :P Шутки в сторону, цикл forбыл - идиома C++, поскольку он назывался "C с классами", и в отличие от строк в стиле C, он никуда не денется. Сам Страуструп предостерег от «попыток писать на C++, как если бы это были $(другие языки)» в своем «Языке программирования C++» (2-е изд.): я не уверен, изменил ли он свою позицию позже, но C++ не совсем функциональный язык. , и пытаясь написать C++ так, как будто это легко может привести к слишком сложному беспорядку.

jick 12.02.2019 19:33

Вероятно, нет никакого вреда в замене одного цикла for на for_each, но глупо утверждать, что последний объективно лучше только потому, что он новый. В C++ нет недостатка в функциях, которые кричат: «То, что вы используете может здесь, не обязательно означает, что это хорошая идея».

jick 12.02.2019 19:33
for_each часто является плохой заменой старой доброй петли for, с этим я полностью согласен. я говорю обо всех остальных 103 алгоритмах.
YSC 12.02.2019 19:34

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