2

I'm creating a game, and trying to learn move semantics/r-value references.

I have a class that adds an Event into a vector every frame.

After 60 frames, I want to move all the accumulated events into a new class (called Step), which will be stored in another vector.

I want this process to repeat, so after the Event vector is moved, it should reset to being empty.

#include <vector>

class Event
{
    ...
}

class Step
{
    std::vector<Event> Events;

    Step(std::vector<Event>&& InEvents) : Events {InEvents} {}
}

class EventAccumulator
{
    std::vector<Event> Events;
    std::vector<Step> Steps;

    void Update(int FrameCount)
    {
        Events.push_back(Event());

        if (FrameCount % 60 == 0)
        {
            // Move accumulated events into a new Step.
            Step NewStep = Step(std::move(Events));
            Steps.push_back(NewStep);

            // Reset Events so that we can accumulate future events.
            Events = std::vector<Event>();
        }
    }
}

// Game Loop
int GlobalFrameCount = 0;
EventAccumulator eventAccumulator{};
while (true)
{
    eventAccumulator.Update(GlobalFrameCount++);
}

It's my understanding that the line Step NewStep = Step(std::move(Events)); will 'give' Events to NewStep (i.e. the vector is not copied). Please correct me if I'm wrong on this.

I want the line Events = std::vector(); to cause EventAccumulator.Events to be reset to an empty vector, but I do NOT want NewStep.Events to be reset.

My question is, will this code do what I want?

Additionally, how can you tell whether this will work/not work for all complex classes like std::vector? I think it's determined by the assignment operator overload of the class, but I'm confused on this.

0liveradam8
  • 752
  • 4
  • 18
  • 1
    The answer to the second half of your question can be found here: https://stackoverflow.com/questions/9168823/reusing-a-moved-container – NathanOliver Feb 01 '21 at 15:29
  • @NathanOliver It's not just that. OP is concerned that by reassigning `Events` it may also clear the newly constructed vector. – Guillaume Racicot Feb 01 '21 at 15:49
  • If you give `Step` a default constructor, a newly constructed `Step` object will have an empty `Events` vector. Then you can use `std::swap(NewStep,Events, Events)` to transfer the vector of `Events into the `NewStep` object and empty the old `Events` object. There are probably better ways to do this, but `std::swap` is the key here. No need for move constructors, etc. – Pete Becker Feb 01 '21 at 19:08

1 Answers1

4

Your code always copy the Events vector, so Events = std::vector(); will simply erase the copied-from elements.

Why does it copy and not a move? Let's take a look at the Step constructor:

//                         that's a copy -----v
Step(std::vector<Event>&& InEvents) : Events {InEvents} {}

Indeed, the expression (InEvents) is an lvalue, because it has a name. You must cast it to an rvalue using std::move:

Step(std::vector<Event>&& InEvents) : Events {std::move(InEvents)} {}

When taking parameters by &&, remember that it's a maybe move since its simply a reference like any other, and you must move it explicitly.


This line won't compile:

Events = std::vector();

Maybe you meant this:

Events = {};

This will indeed allow you to reuse your vector. It will be reset in a way that leads to a determinate state.

I want the line Events = std::vector(); to cause EventAccumulator.Events to be reset to an empty vector, but I do NOT want NewStep.Events to be reset.

My question is, will this code do what I want?

C++ has value semantics. Unless NewStep contains a std::vector<Event>& you cannot affect a variable somewhere else. Also you move construct the vector in NewStep. That should tell you something: You constructed a new vector. No matter how you mutate the old vector, it cannot affect a distinct vector. This code will do what you want, if you correct the Step constructor.


Keep in mind that if you want to avoid many allocation, you will have to call reserve like this:

Events.reserve(60);

As I added in the comments, constructor are a special case for move semantics: taking by value and move it adds very little costs and most likely elided. Here's what I meant by that:

Step(std::vector<Event> InEvents) : Events {std::move(InEvents)} {}

If you pass by copy, then it will copy into InEvents and move it. If you pass by move, it calls the move constructor two times, no copy.

Since the cost of calling the move constructor is negligible, it saves you from writing an overload.

It only works in constructors since we cannot reuse capacity anyway. This is not true for the assignment or a setter function.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • 1
    Note that `Events.clear()` will preserve the vectors capacity (i.e. not lose the allocation), whereas `Events = {}` will not. – peterchen Feb 01 '21 at 15:53
  • @peterchen oh really? I have some code to change then. – Guillaume Racicot Feb 01 '21 at 17:59
  • @peterchen can you call this function on a moved from vector? – Guillaume Racicot Feb 01 '21 at 18:00
  • Yes, but. A moved-from object is in an *unspecified, but valid state*. It may be empty, or have its previous content, or something inbetween. `clear()` guarantees `size()==0`(and, indirectly, that `capacity()` does not change.) **But** you don't know the capacity after the move. The ideal combination for your situation would be `Events.clear(); Events.reserve(...)`. The `reserve` will be a no-op if the move preserved the allocation. – peterchen Feb 01 '21 at 20:51
  • Thanks for the answer. I think I understand what happens now. I thought `std::move` would give ownership of `Events` to the `Step` without doing any copying. But now I see that the stack variables will have to be copied, but only the pointer/reference member variables can be moved without copying the data they point to/reference. The members that are moved is decided by the class's (`vector`'s) move constructor, which we are invoking by using `std::move` (as you describe). Please correct me if I'm wrong – 0liveradam8 Feb 03 '21 at 11:36
  • A follow up question, if I wanted to leave it up to the calling class as to whether `Events` is moved or copied, would I create an extra constructor `Step(const std::vector& InEvents)`, and then the caller would have the option based on whether they use `std::move` or not? – 0liveradam8 Feb 03 '21 at 11:38
  • P.S. I corrected my code, thanks for pointing that out – 0liveradam8 Feb 03 '21 at 11:38
  • @peterchen If I am moving the dynamically-allocated-pointer from `eventAccumulator.Events` to `Step.Events` via the move constructor, then wouldn't both have the same pointer? And `Events.clear()` would clear the shared dynamic memory? Or is it that the `vector` class's move constructor sets `eventAccumulator.Events` to `nullptr` or similar? – 0liveradam8 Feb 03 '21 at 11:45
  • @0liveradam8 `I thought std::move would give ownership of Events to the Step without doing any copying` For that you need to also call `std::move` in the constructor, like I posted in my answer. The function `std::move` don't do anything, it's simply a cast to rvalue. For the move to really kick in, you need to also call `std::move` when calling the vector's constructor. – Guillaume Racicot Feb 03 '21 at 14:33
  • @0liveradam8 `if I wanted to leave it up to the calling class as to whether Events is moved or copied, would I create an extra constructor` Yes, but I wouldn't bother with that. I would rather add a constructor that simply take a vector by value and move it to the member. That would cover both move and copy. – Guillaume Racicot Feb 03 '21 at 14:35
  • @0liveradam8 The move constructor of vector most likely do that, yes. However, I'd expect most move assignment as opposed to move constructor to simply swap the resources. – Guillaume Racicot Feb 03 '21 at 14:36
  • @GuillaumeRacicot `I would rather add a constructor that simply take a vector by value and move it to the member` Wouldn't passing-by-value cause the `vector` to be copied? Let's say `Events` grows very large before being put into a `Step` (much larger than `60`). This copy operation would be very expensive to execute in a single frame of the game. – 0liveradam8 Feb 03 '21 at 15:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/228220/discussion-between-guillaume-racicot-and-0liveradam8). – Guillaume Racicot Feb 03 '21 at 16:14