C++ Command Ownership Model
C++ Ownership Scope Semantics
Unlike languages (such as Java and Python) that hide memory management from the programmer, C++ programmers must be conscious of memory management and object lifetimes.
When allocating on the stack, as following, the object is destroyed when it goes out of scope. In this case, x
is destroyed when MyFunction
returns:
int* MyFunction() {
int x = 3;
// ...
return &x; // invalid pointer -- using it will crash/"segfault"!!!
} // x is destroyed here!
Thus, it is important to store objects in the correct scope so they aren’t destroyed while they’re used – this causes a whole category of C++ bugs called “use after free”.
Allocating on the heap (with new
) persists the object beyond the variable scope, but then the object must be destroyed manually, which is even more difficult to do at the right time: this can cause “use after free” bugs (if the object is destroyed too early) as well as memory leaks (if the object is not destroyed).
In summary, each object should always be _owned_ by a scope that dictates its lifetime and is responsible for automatically destroying it.
Ownership of Command Objects
The C++ Command-based framework often uses two types with vastly different ownership semantics:
Command*
is a raw pointer, non-owning: the command object is owned elsewhere.CommandPtr
is an “smart” owning pointer (wrapper aroundstd::unique_ptr
): whatever owns theCommandPtr
transitively owns the command object.
In other words, functions that take/return a CommandPtr
take/return _ownership_ of the command object, as opposed to functions taking/returning a Command*
that don’t transfer ownership.
For example, the trigger bindings have overloads for both Command*
and CommandPtr
.
Command*: Non-Owning
Here, the command objects are defined as variables of the RobotContainer
class, and therefore are owned by the RobotContainer
object and exist as long as the robot code is running. The variables can be of a Command
subclass (such as InstantCommand
in the case of m_driveHalfSpeed
and m_driveHalfSpeed
), or as CommandPtr
(as in m_spinUpShooter
and m_stopShooter
).
26class RobotContainer { 27 private: 28 frc2::InstantCommand m_driveHalfSpeed{[this] { m_drive.SetMaxOutput(0.5); }, 29 {}}; 30 frc2::InstantCommand m_driveFullSpeed{[this] { m_drive.SetMaxOutput(1); }, 31 {}};
25class RobotContainer { 26 private: 27 // RobotContainer-owned commands 28 // (These variables will still be valid after binding, because we don't move 29 // ownership) 30 31 frc2::CommandPtr m_spinUpShooter = 32 frc2::cmd::RunOnce([this] { m_shooter.Enable(); }, {&m_shooter}); 33 34 frc2::CommandPtr m_stopShooter = 35 frc2::cmd::RunOnce([this] { m_shooter.Disable(); }, {&m_shooter});
To get a Command*
, use &
(address-of operator) in case of a Command
subclass or .get()
in case of a CommandPtr
, and pass it the trigger binding method (such as OnTrue
):
50void RobotContainer::ConfigureButtonBindings() { 51 // Configure your button bindings here 52 53 // While holding the shoulder button, drive at half speed 54 frc2::JoystickButton(&m_driverController, 55 frc::XboxController::Button::kRightBumper) 56 .OnTrue(&m_driveHalfSpeed) 57 .OnFalse(&m_driveFullSpeed); 58}
22void RobotContainer::ConfigureButtonBindings() { 23 // Configure your button bindings here 24 25 // We can bind commands while keeping their ownership in RobotContainer 26 27 // Spin up the shooter when the 'A' button is pressed 28 m_driverController.A().OnTrue(m_spinUpShooter.get()); 29 30 // Turn off the shooter when the 'B' button is pressed 31 m_driverController.B().OnTrue(m_stopShooter.get());
Since the command was passed as a Command*
, ownership is not transferred and the program relies on the command being owned in an appropriate scope. If the command object were to be defined in a different scope and get destroyed, this would be a use-after-free and the program would crash or otherwise misbehave (“Undefined Behavior”).
CommandPtr: Owning
Here, commands are defined as CommandPtr
and _moved_ into the binding, ownership is passed to the scheduler.
22void RobotContainer::ConfigureButtonBindings() {
23 // We can also *move* command ownership to the scheduler
24 // Note that we won't be able to access these commands after moving them!
25
26 // Shoots if the shooter wheel has reached the target speed
27 frc2::CommandPtr shoot = frc2::cmd::Either(
28 // Run the feeder
29 frc2::cmd::RunOnce([this] { m_shooter.RunFeeder(); }, {&m_shooter}),
30 // Do nothing
31 frc2::cmd::None(),
32 // Determine which of the above to do based on whether the shooter has
33 // reached the desired speed
34 [this] { return m_shooter.AtSetpoint(); });
35
36 frc2::CommandPtr stopFeeder =
37 frc2::cmd::RunOnce([this] { m_shooter.StopFeeder(); }, {&m_shooter});
38
39 // Shoot when the 'X' button is pressed
40 m_driverController.X()
41 .OnTrue(std::move(shoot))
42 .OnFalse(std::move(stopFeeder));
Note the calls to std::move
that hint at the ownership move.
The shoot
and stopFeeder
variables will be destroyed when the function returns, but this isn’t a problem because the object was moved (with std::move
) into the function. However, these variables are now in an invalid state and must not be used! Similar to use-after-free, using them would cause crashes or other undefined behavior: this is called use-after-move.
To avoid the risk of use-after-move and invalid variables, CommandPtr
expressions can also be passed inline:
22void RobotContainer::ConfigureButtonBindings() {
23 // We can also define commands inline at the binding!
24 // (ownership will be passed to the scheduler)
25
26 // While holding the shoulder button, drive at half speed
27 m_driverController.RightBumper()
28 .OnTrue(frc2::cmd::RunOnce([this] { m_drive.SetMaxOutput(0.5); }, {}))
29 .OnFalse(frc2::cmd::RunOnce([this] { m_drive.SetMaxOutput(1); }, {}));
It’s also possible to convert Command
subclasses to CommandPtr
using .ToPtr()
:
37 frc2::JoystickButton(&m_joy, 6).OnTrue(CloseClaw(m_claw).ToPtr());
Ownership in Compositions
As described in Command Compositions, command instances that have been passed to a command composition cannot be independently scheduled or passed to a second command composition. In C++, this interacts nicely with the ownership model: each composition owns its components! This way, double-composition bugs are nearly inexistent in C++ (whereas they pose a common error in Java).
Therefore, compositions only take CommandPtr``s and not ``Command*
:
112 return frc2::cmd::Sequence(
113 frc2::InstantCommand(
114 [this, &exampleTrajectory]() {
115 m_drive.ResetOdometry(exampleTrajectory.InitialPose());
116 },
117 {})
118 .ToPtr(),
119 std::move(mecanumControllerCommand),
120 frc2::InstantCommand([this]() { m_drive.Drive(0, 0, 0, false); }, {})
121 .ToPtr());
Ownership of Default Commands
All default commands are owned by the scheduler, therefore, SetDefaultCommand
only takes a CommandPtr
and not a Command*
:
22 m_drive.SetDefaultCommand(frc2::cmd::Run(
23 [this] {
24 m_drive.ArcadeDrive(-m_driverController.GetLeftY(),
25 -m_driverController.GetRightX());
26 },
27 {&m_drive}));