To std::move() or not to std::move() when creating Command Groups

When creating command groups, there appears to be many ways the arguments can be supplied. Sometimes an instance of a command can be added (like m_shootUntilEmpty being a member variable in the robotcontainer, that is an instance of some ShootUntilEmtpy class), other times a class name can be used directly( eg frc2::WaitCommand(3_s) or frc2::PrintCommand(“Some Text”) ) and sometimes parameters need to be std::move(CommandName) like it seems to be the case with an instance of a SwerveControllerCommand.

  1. When and why is the std::move() required when adding commands to a command group?

  2. If you have created one command group and used the std::move() syntax on an instance of a command/command group to add that command/command group to the new command group, can you then create a 2nd new command group that also includes the instance of the command/command group that you std::move()'d in the first command group?

Thanks for the help
Regards
Warren

I can’t speek to Wpilib specifically, though can describe the behavior of std::move. This templated function performs a “cast to r-value”. A brief summary of an “r value” is that it is either a newly constructed object or one which we have indicated we can “move from”.

An object can be passed in a few different ways in C++ (this list is non-exhaustive):
By value: void f(MyObject o) {}
By reference: void f(MyObject& o) {}
By rvalue reference: void f(MyObject&& o) {}

When you pass an object by value, the function will receive a copy of the object which it owns: This could be a copy of something you passed in, or if you called it such as f(MyObject{}), no copy will be made and the ownership simply transferred to the function.

When passing by reference, no copy is made and the ownership refers to the object passed in.

Passing by rvalue reference is similar to passing by reference, but with the distinction that it will only bind to an rvalue reference. On it’s own, passing by rvalue reference doesn’t actually move anything. But it indicates that the callee is free to move from the object if it so chooses.

Some objects cannot be easily copied - there sometimes doesn’t exist logic to sensibly have two copies of the same “thing”. C++ still allows us to pass these around - an object that has a deleted copy constructor can still have a valid move constructor. The move constructor takes an rvalue reference. In order to call a constructor which takes an rvalue reference, we must pass it an object which can bind to an rvalue reference, such as a new instance f(MyObject{}). Alternatively, if we already have an instance which we would like to pass in, we can use std::move. This performs a cast to rvalue reference, which means our lvalue is now an rvalue. So we’ve called the move constructor. This is typically designed to destroy the contents of the passed in object in order to move the internal state of the passed in object to the new object. So the object my_object we passed to f(std::move(my_object)) contains a valid but indeterminate state, typically some form of “empty”. For instance, a moved from vector often has size 0, and a move from unique_ptr is nullptr.

To answer your questions:

  1. std::move is required because the command doesn’t have a copy constructor, only a move constructor. When adding a command, by wrapping your object in std::move you indicate you don’t need it any more and the callee can take logical ownership of its state. The command object you still have is now “empty”. (I’m not sure how wpilib defines this, but you should assume it’s garbage and should either be carefully reset or not reused at all).

  2. If you move a command group into another command with std::move, the original command object is likely “empty”. So you cannot reuse the same instance.

Now, I’m not that familiar with the wpilib api, but if you really wanted to, you *might* be able to use a shared_ptr to share a command’s ownership. But this is usually considered a code smell. I’ll let @calcmogul comment on this.

1 Like

You can’t. The command must be moved to transfer ownership.

1 Like

Thanks for info. Unfortunately I’m still not 100% clear on why one instance of a command group can be added to another command group by just suppling the instance name and others need to be std::move()'d. A pattern I’ve noticed is if the command group to be added to another commang group contains commands that take lambda, these ones need to std::move(), where as a command group that is made up of commands that only take pointers to subsystems appear to be able to be added without the std::move(). Is this a fare assumption?

The difference is the way things are passed. C++ differentiates between lvalues and rvalues. Historically they were called lvalue and rvalue as lvalues were the “left” side of an assignment expression and rvalues were the “right” side of an assignment expression, but it’s a little more complicated than that. rvalues are “temporary” values. A normal variable when passed to a function is a lvalue. The return value of a function call is a rvalue. std::move() converts a lvalue into a rvalue. So you don’t need std::move in something like:

frc2::SequentialCommandGroup{FooCommand(), BarCommand()};

Because FooCommand() and BarCommand() are already rvalues.

However, if you assign a command to a variable, and then pass it to something expecting a rvalue, you need to use std::move:

FooCommand cmd1;
BarCommand cmd2;
frc2::SequentialCommandGroup{std::move(cmd1), std::move(cmd2)};

frc2::SequentialCommandGroup{cmd1, cmd2};  // not allowed, would try to copy

The way command groups are designed requires the parameters be rvalues, so you need to use std::move() to tell the compiler you’re okay with treating a lvalue as a rvalue (and this also tells you as the programmer that it’s most likely not safe to use the same variable later, as it’s been moved-from).

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.