Я задавался вопросом, является ли это подходящей практикой:
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
и добавить/удалить элементы, поэтому мне пришлось придумать это решение.
Кажется ли это хорошей идеей?
Я думаю, что вы слишком много усложняете. Если вы находитесь в многопоточной ситуации (вы не упомянули об этом в своем вопросе), вам наверняка понадобятся блокировки, защищающие чтения от других потоков, которые обращаются к вашим измененным спискам. Поскольку в стандартной библиотеке нет параллельных структур данных, вам нужно будет добавить такой материал самостоятельно.
Для однопоточного кода вы можете просто вызвать элемент 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
элементы не были удалены, так как каждый из них по- прежнему имеют общий указатель, связанный с ними. Только если количество повторений будет уменьшено до нуля, будут ли эти прежние элементы списка удалены.
Если бы я просмотрел этот код, я бы сказал, что это неприемлемо.
Какова цель двухэтапного удаления? Необычное решение, подобное этому, требует комментариев, объясняющих его цель. Несмотря на неоднократные просьбы, вы не смогли объяснить это.
Идея здесь в том, что мы отмечаем, что контейнер
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
цикла для перебора по списку?
Наконец, почему все названо с большой буквы?! Именование локальных переменных и функций с большой буквы является просто странным, и если все названо таким образом, то это бессмысленно, потому что капитализация не помогает различать разные типы имен (например, используя заглавную букву только для типов, дает понять, какие имена типы, а не... использование его для всего бесполезно.)
Я чувствую, что это только усложняет ситуацию, потому что нужно везде проверять 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);
}
Items.clear();
Кроме того, что не так с std :: moving shared_ptr? Если я не переместлю его, будет создана другая копия, похоже на пустую трата времени.remove_if
, но, похоже, есть некоторое ограничение, которое вы не упоминаете.