Search code examples
c++visual-c++c++17move-semanticsrvalue-reference

Can I re-use a complex class like std::vector after std::move?


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.


Solution

  • 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.