0

This is my code:

std::list<GameMatch*> games_;

void GameMatchManager::ClearEndedGames()
{
    if(games_.empty())
        return;

    auto it = games_.begin();
    while (it != games_.end())
    {
        auto game = *it;
        if(game->MatchEnded())
        {
            games_.erase(it);
            game->Destroy();
        }
        ++it;
    }
}

void GameMatch::Destroy()
{
    std::cout << "Destoying " << GetMatchId() << std::endl;
    delete this;
}

This is the error I am getting:

enter image description here

Why am I getting this error in first place when at the top i do if(games.empty()) return; ?

Shouldn't that stop the iteration of games_ list ? Why am I getting this error and how can I fix it?

Venelin
  • 2,905
  • 7
  • 53
  • 117
  • Please post code, errors, sample data or textual output here as plain-text, not as images that can be hard to read, can’t be copy-pasted to help test code or use in answers, and are barrier to those who depend on screen readers or translation tools. You can edit your question to add the code in the body of your question. For easy formatting use the `{}` button to mark blocks of code, or indent with four spaces for the same effect. The contents of a **screenshot can’t be searched, run as code, or easily copied and edited to create a solution.** – tadman Jun 15 '23 at 17:59
  • 2
    Read about [`std::list::erase`](https://en.cppreference.com/w/cpp/container/list/erase). Pay attention to what it returns. The link even contains an example that you should study. – Ted Lyngmo Jun 15 '23 at 18:00
  • https://stackoverflow.com/questions/596162/can-you-remove-elements-from-a-stdlist-while-iterating-through-it – interjay Jun 15 '23 at 18:01
  • 2
    You should use a regular destructor initiated via `delete` instead of whatever `Destroy` is masquerading as. Even better, if you can get rid of pointers entirely here then `erase` is all you need. – tadman Jun 15 '23 at 18:01
  • You can actually do all of this using no `while` loop and no finagling with `it` by using `stable_partition`, `for_each` and then `list::erase`, essentially 3 lines of code. – PaulMcKenzie Jun 15 '23 at 18:11
  • 1
    The issue is occurring because you are deleting the object `game` points to while it is still being used in the loop. After the call to`games_.erase(it);`, the iterator `it` is invalidated, meaning you can't use `it` any more. But in the next line, you increment `it` with `++it;`, which is undefined behavior since `it` has been invalidated. – elmiomar Jun 15 '23 at 18:13
  • @elmiomar even if I remove `game->Destroy();` again it produces same error. Any idea ? – Venelin Jun 15 '23 at 18:15
  • @Venelin For the same reason that elmiomar stated. This has nothing to do with destroying game objects, and everything to do with using an iterator after it has been invalidated. – john Jun 15 '23 at 18:21
  • @Venelin try storing the next `it` before erasing the current one using `auto next_it = std::next(it);`, then after you call `game->Destroy();` move to the next `it` using `it = next_it;`. And add an else for `++it`. – elmiomar Jun 15 '23 at 18:22
  • @Venelin -- [Here is an example using what I mentioned in the comment](https://godbolt.org/z/P78cr9Wd7). – PaulMcKenzie Jun 15 '23 at 18:22
  • 1
    @Venelin This is how you code it correctly `if(game->MatchEnded()) { it = games_.erase(it); game->Destroy(); } else { ++it; }` – john Jun 15 '23 at 18:23
  • @john if I do that now I get crash on `games_.erase(it);` https://imgur.com/a/bSFOj3U – Venelin Jun 15 '23 at 18:58
  • @elmiomar is it enough if I use the increase of iterator in the else as I have done in the above's comment ? – Venelin Jun 15 '23 at 19:00
  • @john thank you sir. I saw the error. I forgot `it = ` on the line with `games_.erase(it);` – Venelin Jun 15 '23 at 19:13

1 Answers1

1

The obvious problem: You are using an invalid iterator. When you delete an element from a std::list, iterators pointing at other elements remain valid, but the deleted element’s iterator is in an undefined state. A possible solution is to iterate one step ahead of deletions:

template <typename List>
void ConsumeList(List&& list) {
  auto last_it = list.begin();
  if (last_it != list.end()) {
    for (auto it = std::next(last_it); it != list.end(); ++it) {
      list.erase(last_it);  // last_it becomes invalid here.
      last_it = it;         // Never mind, we make it valid again.
    }
    list.erase(last_it);    // Don't forget this!
  }
}

Other notes, mostly unrelated to the problem:

  1. Avoid methods such as Destroy(); it is likely doing something that ~GameMatch() should be doing instead, but without strong RAII-related guarantees from the compiler. Prefer RAII over “homebrew” resource management.
  2. Do not use raw pointers. You almost never(*) need them.
    (*) The only exception is when you implement low-level linked data structures. Then (and only then) it makes sense to use raw pointers.
  3. Preferably, do not allocate objects directly (using smart pointers or otherwise) at all. Instead, let STL containers handle allocation. Embed your objects directly (std::list<GameMatch> instead of std::list<GameMatch*>). The fewer pointer jumps, the better efficiency you get.

Here’s ConsumeList() once again, this time in a buildable and runnable context:

#include <iostream>
#include <iterator>
#include <list>
#include <memory>
#include <string>
#include <string_view>
#include <utility>

namespace {

struct GameMatch {
  GameMatch(std::string_view match_id) : match_id_{match_id} {}
  std::string_view GetMatchId() const { return match_id_; }
  ~GameMatch() { std::cout << "Destoying " << GetMatchId() << std::endl; }

 private:
  const std::string match_id_;
};

template <typename List>
void ConsumeList(List&& list) {
  auto last_it = list.begin();
  if (last_it != list.end()) {
    for (auto it = std::next(last_it); it != list.end(); ++it) {
      list.erase(last_it);
      last_it = it;
    }
    list.erase(last_it);
  }
}

}  // namespace

int main() {
  std::list<GameMatch> embedded_list;
  embedded_list.emplace_back("Embedded Match A");
  embedded_list.emplace_back("Embedded Match B");
  embedded_list.emplace_back("Embedded Match C");

  std::list<std::unique_ptr<GameMatch>> pointer_list;
  pointer_list.emplace_back(std::make_unique<GameMatch>("Allocated Match A"));
  pointer_list.emplace_back(std::make_unique<GameMatch>("Allocated Match B"));
  pointer_list.emplace_back(std::make_unique<GameMatch>("Allocated Match C"));
  
  ConsumeList(std::move(embedded_list));
  ConsumeList(std::move(pointer_list));
}

Notice that forgetting to run ConsumeList() will not cause any memory leaks at the end and all list elements will be properly deallocated. That is the key advantage of using RAII and avoiding raw pointers.

Andrej Podzimek
  • 2,409
  • 9
  • 12