C ++ Маркировка объектов для удаления в списке STD через nullptrs

0

Я задавался вопросом, является ли это подходящей практикой:

struct Item { };
std::list<std::shared_ptr<Item>> Items;
std::list<std::shared_ptr<Item>> RemovedItems;

void Update()
{
    Items.push_back(std::make_shared<Item>()); // sample item

    for (auto ItemIterator=Items.begin();ItemIterator!=Items.end();ItemIterator++)
    {
        if (true) { // a complex condition, (true) is for demo purposes
            RemovedItems.push_back(std::move(*ItemIterator)); // move ownership
            *ItemIterator=nullptr; // set current item to nullptr
        }

        // One of the downsides, is that we have to always check if
        // the current iterator value is not a nullptr
        if (*ItemIterator!=nullptr) {
            // A complex loop where Items collection could be modified
        }
    }

    // After the loop is done, we can now safely remove our objects

    RemovedItems.clear(); // calls destructors on objects

    //finally clear the items that are nullptr
    Items.erase( std::remove_if( Items.begin(), Items.end(),
        [](const std::shared_ptr<Item>& ItemToCheck){
            return ItemToCheck==nullptr;
    }), Items.end() );
}

Идея здесь в том, что мы отмечаем, что контейнер Items может быть использован внешними источниками. Когда элемент удаляется из контейнера, он просто устанавливает значение nullptr, но до этого перемещается в RemovedItems.

Что-то вроде события может повлиять на Items и добавить/удалить элементы, поэтому мне пришлось придумать это решение.

Кажется ли это хорошей идеей?

Теги:
list
c++11
vector
erase-remove-idiom

3 ответа

2
Лучший ответ

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

Для однопоточного кода вы можете просто вызвать элемент std:list remove_if с вашим предикатом. Нет необходимости указывать указатели на нуль, хранить их и делать несколько проходов над вашими данными.

#include <algorithm>
#include <list>
#include <memory>
#include <iostream>

using Item = int;

int main()
{
    auto lst = std::list< std::shared_ptr<Item> > 
    { 
        std::make_shared<int>(0), 
        std::make_shared<int>(1), 
        std::make_shared<int>(2), 
        std::make_shared<int>(3),         
    };    

    // shared_ptrs to even elements
    auto x0 = *std::next(begin(lst), 0);
    auto x2 = *std::next(begin(lst), 2);

    // erase even numbers
    lst.remove_if([](std::shared_ptr<int> p){
        return *p % 2 == 0;    
    });

    // even numbers have been erased
    for (auto it = begin(lst); it != end(lst); ++it)
        std::cout << **it << ",";    
    std::cout << "\n";

    // shared pointers to even members are still valid
    std::cout << *x0 << "," << *x2;
}

Живой пример.

Обратите внимание, что элементы были действительно стерты из списка, а не просто в конце списка. Последним эффектом является то, что будет делать стандартный алгоритм std::remove_if, после чего вам нужно будет вызвать erase функции члена std::list. Эта двухэтапная стирание-удаление идиомы выглядит так:

// move even numbers to the end of the list in an unspecified state
auto res = std::remove_if(begin(lst), end(lst), [](std::shared_ptr<int> p){
    return *p % 2 == 0;    
});

// erase even numbers
lst.erase(res, end(lst));

Живой пример.

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

1

Если бы я просмотрел этот код, я бы сказал, что это неприемлемо.

Какова цель двухэтапного удаления? Необычное решение, подобное этому, требует комментариев, объясняющих его цель. Несмотря на неоднократные просьбы, вы не смогли объяснить это.

Идея здесь в том, что мы отмечаем, что контейнер Items может быть использован внешними источниками.

Вы имеете в виду "Идея здесь в том, что пока мы отмечаем, что контейнер Items может быть использован внешними источниками". ? В противном случае это предложение не имеет смысла.

Как это может быть затронуто? Ваше объяснение непонятно:

Подумайте о Root → Parent → Child отношениях. Событие может возникнуть у Child который может удалить Parent из Root. Таким образом, цикл может сломаться в середине, и итератор будет недействительным.

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

"Отношения между родителем и ребенком" могут означать много разных вещей. Вы имеете в виду, что типы связаны, по наследству? Объекты связаны, по собственному желанию? Какие?

Какое "событие"? Событие может означать много вещей, я хочу, чтобы люди в StackOverflow перестали использовать слово "событие" для обозначения конкретных вещей и предполагая, что все остальные знают, какой смысл они намереваются. Вы имеете в виду асинхронное событие, например, в другом потоке? Или вы имеете в виду, что уничтожение Item может привести к удалению других элементов из списка Items?

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

Основываясь на этом комментарии:

//A complex loop where Items collection could be modified

Я предполагаю, что вы не имеете в виду асинхронное событие (но тогда почему вы говорите, что "внешние источники" могут изменять контейнер), и в этом случае ваше решение гарантирует, что итераторы остаются в силе, пока "сложный цикл" повторяется над списком, но почему действительно нужны фактические Item объекты остаются в силе, а не просто держать итераторы в силе? Не могли бы вы просто установить элемент в nullptr не помещая его в RemovedItems, а затем сделать Items.remove_if([](shared_ptr<Item> const& p) { return !p; } в конце? Вам нужно объяснить немного больше о что ваш "сложный цикл" может сделать с контейнером или с элементами.

Почему RemovedItems не является локальной переменной в функции Update()? Кажется, что это не требуется вне этой функции. Почему бы не использовать новый цикл С++ 11 for цикла для перебора по списку?

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

-3

Я чувствую, что это только усложняет ситуацию, потому что нужно везде проверять nullptr. Кроме того, перемещение shared_ptr немного глупо.

редактировать:

Я думаю, что сейчас понимаю проблему, и именно так я бы ее решил:

struct Item {
    std::list<std::shared_ptr<Item>> Children;
    std::set < std::shared_ptr<Item>, std::owner_less < std::shared_ptr<Item >> > RemovedItems;
    void Update();
    void Remove(std::shared_ptr<Item>);
};

void Item::Update()
{
    for (auto child : Children){
        if (true) { // a complex condition, (true) is for demo purposes
            RemovedItems.insert(child);
        }
        // A complex loop where children collection could be modified but
        // only by calling Item::remove, Item::add or similar
    }
    auto oless = std::owner_less < std::shared_ptr < Item >>();
    std::sort(Children.begin(), Children.end(), oless ); //to avoid use a set

    auto newEnd = std::set_difference(Children.begin(),
        Children.end(),
        RemovedItems.begin(),
        RemovedItems.end(),
        Children.begin(),
        oless);
    Children.erase(newEnd, Children.end());

    RemovedItems.clear(); // may call destructors on objects

}

void Item::Remove(std::shared_ptr<Item> element){
    RemovedItems.insert(element);
}
  • 1
    это потерпит неудачу, если сложный цикл сделает что-то вроде Items.clear(); Кроме того, что не так с std :: moving shared_ptr? Если я не переместлю его, будет создана другая копия, похоже на пустую трата времени.
  • 0
    Ну, тогда вы можете переместить цикл после remove_if , но, похоже, есть некоторое ограничение, которое вы не упоминаете.
Показать ещё 12 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню