0

I have a game with non-copyable Items as they are supposed to be unique:

class Item {
  Item() noexcept;
  Item(Item&&) noexcept;
  Item(Item const&) = delete;
// ...
};

class Creature is capable of receiving an Item and adding it to their inventory:

void Creature::receive_item(Item&& item) noexcept {
  this->inventory.push_back(std::move(item));
}

void caller_code() {
  Creature creature;
  Item item;
  // ...
  creature.receive_item(std::move(item));
}

This approach seems nice and clean but there's a small problem: if any part of my code can recover after std::bad_alloc and therefore catches one thrown by recieve_item()'s push_back(), the in-game logic is undefined: an Item was moved to a function which failed storing one so it is just lost. Moreover, the noexcept specifier has to be removed just for this possibility.

So, I can provide exception safety this way (please point it out if I'm wrong):

void Creature::receive_item(Item& item) {
  this->inventory.resize(this->inventory.size() + 1);
  // if it needs to allocate and fails, throws leaving the item untouched
  this->inventory.back() = std::move(item);
}

void caller_code() {
  Creature creature;
  Item item;
  // ...
  creature.receive_item(item); // no moving
}

However, now we have new disadvantages:

  • the lack of std::move() in the caller's code obscures the fact of moving the item;
  • the "resize(+1)" part is just ugly and may be misunderstood during a code review.

The question is in the title. Is exception safety even for such a weird case considered a good design?

passing_through
  • 1,778
  • 12
  • 24
  • 3
    Can you really recover from a `std::bad_alloc`? If inserting a single element into a vector runs you out of memory I don't see what chance you have of recovering. – NathanOliver Feb 13 '20 at 17:08
  • This might be a better question for the Code Review site on StackExchange. – T_Bacon Feb 13 '20 at 17:09
  • @NathanOliver `vector` usually allocates x1.5 or x2 more (as far as I know) so the difference might be serious, I think. – passing_through Feb 13 '20 at 17:10
  • @passing_through Yes, but does your creature really have that large of an inventory where double the size would be an appreciable amount of memory? – NathanOliver Feb 13 '20 at 17:12
  • @NathanOliver I'm not really expecting it to do. However, if later I decide I need such type of exception safety, it is too much code to investigate, fully understand and rewrite, so I don't want a late decision here. – passing_through Feb 13 '20 at 17:14
  • @passing_through remember that on Linux, and some other Operating Systems, you will, by default, not even get your `std::bad_alloc` at allocation time since those systems overcommit memory and only *actually* allocate it when you write to it. Don't depend on `std::bad_alloc`. – Jesper Juhl Feb 13 '20 at 17:27
  • Out-of-memory is so difficult to deal on modern OS's that probably the simplest and most robust solution is simply to let the process crash when out-of-memory occurs, and have a parent "babysitter" process around to relaunch it afterwards. That has the nice side effect of allowing the OS to clean up any leaked memory/resources that might have led to the out-of-memory condition in the first place. (As @JesperJuhl notes above, the crash is likely to happen *anyway* due to the OOM-killer coming for your process, so you'll have to handle that in any case, so why not design for it?) – Jeremy Friesner Feb 13 '20 at 17:27
  • @JesperJuhl @JeremyFriesner so I can be assured that handling out of memory error is hardly possible (OS-implementaton-defined) in the way it is in the code example, can't I? Should I just mark `recieve_item()` as `noexcept` and receiving `&&` like in the first code piece? – passing_through Feb 13 '20 at 17:35
  • Note that doing `reserve/resize(size() + 1)` can really mess up your code's performance (which in this case might actually matter, since it's for a game). Instead of approx. log(N) reallocations, there will be N reallocations/moves of all elements (where N == number of insertions). – AVH Feb 13 '20 at 17:37
  • @Darhuuk neither `reserve()` nor `resize()` seems to be actually allocating every time. Or is it implementation-defined, as usual? – passing_through Feb 13 '20 at 17:38
  • @passing_through When inserting new elements, `vector` will normally allocate memory by doubling the storage size, which means log(N) memory reallocations. If you instead prepend the insertion with `reserve(size + 1)`, you're forcing it to reallocate on every insertion, because each new element will exceed the vector capacity. – Andrey Semashev Feb 13 '20 at 17:43
  • @AndreySemashev but I've written `resize()`, not `reserve()`. I suppose this one allocates only if it has no free space hidden after `end()`, am I wrong? – passing_through Feb 13 '20 at 17:47
  • @passing_through I suppose, you should look in your standard library docs and source code to see how `resize` is implemented, but I would not expect it to allocate memory for elements beyond the number of elements requested. I.e. I would expect the behavior similar to `reserve`. This is all implementation-specific. – Andrey Semashev Feb 13 '20 at 17:51
  • to reach situation where `std::bad_alloc` is thrown lots of memory has to be allocated (~3GB for 32 bit app). Most probably your code leaks memory or you are acquire ridiculously large pieces of memory. – Marek R Feb 13 '20 at 17:51
  • @MarekR I'm not getting out of memory right now; the question arised as I was thinking about the future of this project. Of course, I'm not really expecting such errors to occur, the main discussion here is exception safety for such cases. – passing_through Feb 13 '20 at 17:54

1 Answers1

1

The answer to your question depends on whether you can and want to handle memory allocation errors, other than by crashing. If you do, you will have to implement measures beyond just catching std::bad_alloc. Most modern operating systems implement memory overcommit, which means memory allocation will succeed, but accessing the allocated memory for the first time will cause a page fault, which will normally result in a crash. You have to explicitly fault allocated pages in your memory allocator to detect out of memory condition before returning the pointer to the caller.

Regarding your code modification, you don't have to modify your call to push_back:

this->inventory.push_back(std::move(item));

If push_back (or any other potentially reallocating method) needs to allocate a new buffer, it will do this before moving the new item into the vector. Obviously, this is because there is no room in the vector to move the new element to. And when the buffer is reallocated and all existing elements are moved/copied to the new buffer, the new element is moved as the final step.

In other words, if push_back throws, you can be sure the new item has not been moved. If it doesn't throw and returns normally, the item is moved from.

Andrey Semashev
  • 10,046
  • 1
  • 17
  • 27
  • I assume `std::move()` is executed before `push_back()` in this case because the latter gets the `item` as a parameter so moving seems to be obligatory for just performing the actual function call. Are you sure I'm wrong? – passing_through Feb 13 '20 at 17:31
  • 4
    `std::move` does not copy or move anything, it's just a syntax sugar for a `static_cast< T&& >` so that `push_back` is called with an rvalue reference argument rather than lvalue reference. – Andrey Semashev Feb 13 '20 at 17:33
  • Oh really, this is what has just become obvious for me :D I even tested it a minute ago. Thanks for accidentally filling an extra hole in my knowledge. – passing_through Feb 13 '20 at 17:44