C++ Komutları Üzerine Teknik Bir Tartışma

Not

This article assumes that you have a fair understanding of advanced C++ concepts, including templates, smart pointers, inheritance, rvalue references, copy semantics, move semantics, and CRTP. You do not need to understand the information within this article to use the command-based framework in your robot code.

This article will help you understand the reasoning behind some of the decisions made in the 2020 command-based framework (such as the use of std::unique_ptr, CRTP in the form of CommandHelper<Base, Derived>, etc.). You do not need to understand the information within this article to use the command-based framework in your robot code.

Not

The model was further changed in 2023, as described below.

Sahiplik Modeli

Eski komut tabanlı framework, ham işaretçilerin kullanımını kullanıyordu, bu da kullanıcıların robot kodlarında new -yeni (manuel öbek ayrımlarıyla sonuçlanan) kullanmaları gerektiği anlamına geliyordu. Komutların kime ait olduğuna dair net bir gösterge olmadığından (zamanlayıcı, komut grupları veya kullanıcının kendisi), hafızayı boşaltmakla kimin ilgileneceği belli değildi.

Eski komut tabanlı framework’teki birkaç örnek şu şekilde kod içeriyordu:

#include "PlaceSoda.h"
#include "Elevator.h"
#include "Wrist.h"

PlaceSoda::PlaceSoda() {
  AddSequential(new SetElevatorSetpoint(Elevator::TABLE_HEIGHT));
  AddSequential(new SetWristSetpoint(Wrist::PICKUP));
  AddSequential(new OpenClaw());
}

In the command-group above, the component commands of the command group were being heap allocated and passed into AddSequential all in the same line. This meant that user had no reference to that object in memory and therefore had no means of freeing the allocated memory once the command group ended. The command group itself never freed the memory and neither did the command scheduler. This led to memory leaks in robot programs (i.e. memory was allocated on the heap but never freed).

Bu göze batan sorun, framework-çerçevenin yeniden yazılmasının nedenlerinden biriydi. Bu yeniden yazma ile birlikte kapsamlı bir sahiplik modeli ve kapsam dışına çıktıklarında hafızayı otomatik olarak boşaltacak akıllı işaretçilerin kullanımıyla birlikte tanıtıldı.

Default commands are owned by the command scheduler whereas component commands of command compositions are owned by the command composition. Other commands are owned by whatever the user decides they should be owned by (e.g. a subsystem instance or a RobotContainer instance). This means that the ownership of the memory allocated by any commands or command compositions is clearly defined.

std::unique_ptr vs. std::shared_ptr

std::unique_ptr kullanmak, nesneye kimin sahip olduğunu açıkça belirlememizi sağlar. Bir std::unique_ptr kopyalanamadığı için, öbek üzerinde aynı bellek bloğunu işaret eden bir std::unique_ptr nin birden fazla örneği olmayacaktır. Örneğin, SequentialCommandGroup için bir constructor -yapıcı, bir std::vector<std::unique_ptr<Command>>&& alır. Bu, std::unique_ptr<Command> vektörüne bir rvalue referansı gerektirdiği anlamına gelir. Bunu daha iyi anlamak için bazı örnek kodları adım adım inceleyelim:

// Let's create a vector to store our commands that we want to run sequentially.
std::vector<std::unique_ptr<Command>> commands;

// Add an instant command that prints to the console.
commands.emplace_back(std::make_unique<InstantCommand>([]{ std::cout << "Hello"; }, requirements));

// Add some other command: this can be something that a user has created.
commands.emplace_back(std::make_unique<MyCommand>(args, needed, for, this, command));

// Now the vector "owns" all of these commands. In its current state, when the vector is destroyed (i.e.
// it goes out of scope), it will destroy all of the commands we just added.

// Let's create a SequentialCommandGroup that will run these two commands sequentially.
auto group = SequentialCommandGroup(std::move(commands));

// Note that we MOVED the vector of commands into the sequential command group, meaning that the
// command group now has ownership of our commands. When we call std::move on the vector, all of its
// contents (i.e. the unique_ptr instances) are moved into the command group.

// Even if the vector were to be destroyed while the command group was running, everything would be OK
// since the vector does not own our commands anymore.

std::shared_ptr ile, açık bir sahiplik modeli yoktur çünkü aynı bellek bloğunu işaret eden bir std::shared_ptr nin birden fazla örneği olabilir. Komutlar std::shared_ptr örneklerinde olsaydı, bir komut grubu veya komut zamanlayıcı, komutun yürütülmesi bittikten sonra sahipliği alamaz ve belleği boşaltamaz çünkü kullanıcı hala kapsamı içinde bir yerdeki bellek bloğuna işaret eden örnek std::shared_ptr ye sahip olabilir.

CRTP kullanımı

You may have noticed that in order to create a new command, you must extend CommandHelper, providing the base class (usually frc2::Command) and the class that you just created. Let’s take a look at the reasoning behind this:

Komut Dekoratörleri

Yeni komut tabanlı framework, kullanıcının aşağıdaki gibi bir şey yapmasına olanak tanıyan “komut dekoratörleri” olarak bilinen bir özelliği içerir:

auto task = MyCommand().AndThen([] { std::cout << "This printed after my command ended."; },
  requirements);

task -görev zamanlandığında, ilk önce MyCommand() çalıştıracak ve bu komutun yürütülmesi bittiğinde, mesajı konsola yazdıracaktır. Bunun dahili olarak elde etmenin yolu, sıralı bir komut grubu kullanmaktır.

Bir önceki bölümden, sıralı bir komut grubu oluşturmak için her komuta benzersiz bir işaretçi vektörüne ihtiyacımız olduğunu hatırlayın. Yazdırma işlevi için benzersiz bir işaretçi oluşturmak oldukça basittir:

temp.emplace_back(
   std::make_unique<InstantCommand>(std::move(toRun), requirements));

Burada temp , SequentialCommandGroup yapıcısına iletmemiz gereken komut vektörünü depoluyor. Ancak InstantCommand``i eklemeden önce, ``SequentialCommandGroup a, MyCommand() eklememiz gerekiyor. Bunu nasıl yaparız?

temp.emplace_back(std::make_unique<MyCommand>(std::move(*this));

You might think it would be this straightforward, but that is not the case. Because this decorator code is in the Command class, *this refers to the Command in the subclass that you are calling the decorator from and has the type of Command. Effectively, you will be trying to move a Command instead of MyCommand. We could cast the this pointer to a MyCommand* and then dereference it but we have no information about the subclass to cast to at compile-time.

Soruna Yönelik Çözümler

Buna ilk çözümümüz, Command içinde TransferOwnership() adlı, Command in her alt sınıfının geçersiz kılmak zorunda olduğu sanal bir yöntem oluşturmaktı. Böyle bir geçersiz kılma şuna benzerdi:

std::unique_ptr<Command> TransferOwnership() && override {
  return std::make_unique<MyCommand>(std::move(*this));
}

Kod türetilmiş alt sınıfta olacağından, *this aslında istenen alt sınıf örneğini işaret eder ve kullanıcı, benzersiz işaretçiyi yapmak için türetilmiş sınıfın tür bilgisine sahiptir.

Birkaç günlük değerlendirmeden sonra, bir CRTP yöntemi önerildi. Burada, CommandHelper adı verilen, aracı olarak türetilmiş bir Command sınıfı mevcut olacaktır. CommandHelper iki şablon argümanına sahip olacaktır, orijinal temel sınıf ve istenen türetilmiş alt sınıf. Bunu anlamak için CommandHelper’ ın temel uygulamasına bir göz atalım:

// In the real implementation, we use SFINAE to check that Base is actually a
// Command or a subclass of Command.
template<typename Base, typename Derived>
class CommandHelper : public Base {
  // Here, we are just inheriting all of the superclass (base class) constructors.
  using Base::Base;

  // Here, we will override the TransferOwnership() method mentioned above.
  std::unique_ptr<Command> TransferOwnership() && override {
    // Previously, we mentioned that we had no information about the derived class
    // to cast to at compile-time, but because of CRTP we do! It's one of our template
    // arguments!
    return std::make_unique<Derived>(std::move(*static_cast<Derived*>(this)));
  }
};

Bu nedenle, özel komutlarınızı Command yerine CommandHelper kapsamında yapmak, bu standart levhayı -boilerplate sizin için otomatik olarak uygulayacaktır ve ekiplerden işleri yapmak için oldukça belirsiz gibi görünen bir yöntemi kullanmalarını istemenin arkasındaki sebep budur.

AndThen() örneğimize geri dönersek, şimdi aşağıdakileri yapabiliriz:

// Because of how inheritance works, we will call the TransferOwnership()
// of the subclass. We are moving *this because TransferOwnership() can only
// be called on rvalue references.
temp.emplace_back(std::move(*this).TransferOwnership());

Gelişmiş Dekoratörlerin Eksikliği

C++ dekoratörlerinin çoğu, gerçek komutların kendileri yerine std::function<void()> alır. Dekoratörlerde AndThen(), BeforeStarting() vb. gibi gerçek komutları alma fikri düşünüldü, ancak daha sonra çeşitli nedenlerle terk edildi.

Dekoratörleri Şablonlama

Derleme zamanında, bir komut grubuna eklediğimiz komutların türlerini bilmemiz gerektiğinden, şablonları (çoklu komutlar için değişken) kullanmamız gerekecek. Ancak, bu büyük bir uğraş gibi görünmeyebilir. Komut grupları için yapıcılar bunu yine de yapar:

template <class... Types,
         typename = std::enable_if_t<std::conjunction_v<
             std::is_base_of<Command, std::remove_reference_t<Types>>...>>>
explicit SequentialCommandGroup(Types&&... commands) {
  AddCommands(std::forward<Types>(commands)...);
}

template <class... Types,
         typename = std::enable_if_t<std::conjunction_v<
             std::is_base_of<Command, std::remove_reference_t<Types>>...>>>
void AddCommands(Types&&... commands) {
  std::vector<std::unique_ptr<Command>> foo;
  ((void)foo.emplace_back(std::make_unique<std::remove_reference_t<Types>>(
       std::forward<Types>(commands))),
   ...);
  AddCommands(std::move(foo));
}

Not

Bu, yukarıda tanımladığımız vektör yapıcısına ek olarak SequentialCommandGroup için ikincil bir kurucudur.

Bununla birlikte, şablonlanmış bir fonksiyon yaptığımızda, tanımı satır içi-inline olarak bildirilmelidir. Bu, bir sorun oluşturan Command.h başlığındaki SequentialCommandGroup örneğini oluşturmamız gerektiği anlamına gelir. SequentialCommandGroup.h, Command.h içerir. Command.h içerisine SequentialCommandGroup.h eklersek, döngüsel bir bağımlılığımız olur. Şimdi nasıl yapacağız peki?

Command.h` başlığının üstünde ileriye dönük bir bildirim kullanıyoruz:

class SequentialCommandGroup;

class Command { ... };

Ve sonra SequentialCommandGroup.h yi Command.cpp içine dahil ediyoruz. Bu dekoratör işlevleri şablon haline getirildiyse, .cpp dosyalarına tanım yazamayız ve bu da döngüsel bir bağımlılıkla sonuçlanır.

Java vs. C++ Sözdizimi Kuralları

Bu dekoratörler genellikle Java’da C++’dan daha fazla ayrıntıyı kaydeder (çünkü Java, ham new -yeni çağrılar gerektirir), bu nedenle genel olarak; komut grubunu, kullanıcı kodunda manuel olarak oluşturursanız, C++’ da söz dizimsel bir fark yaratmaz.

2023 Updates

After a few years in the new command-based framework, the recommended way to create commands increasingly shifted towards inline commands, decorators, and factory methods. With this paradigm shift, it became evident that the C++ commands model introduced in 2020 and described above has some pain points when used according to the new recommendations.

A significant root cause of most pain points was commands being passed by value in a non-polymorphic way. This made object slicing mistakes rather easy, and changes in composition structure could propagate type changes throughout the codebase: for example, if a ParallelRaceGroup were changed to a ParallelDeadlineGroup, those type changes would propagate through the codebase. Passing around the object as a Command (as done in Java) would result in object slicing.

Additionally, various decorators weren’t supported in C++ due to reasons described above. As long as decorators were rarely used and were mainly to reduce verbosity (where Java was more verbose than C++), this was less of a problem. Once heavy usage of decorators was recommended, this became more of an issue.

CommandPtr

Let’s recall the mention of std::unique_ptr far above: a value type with only move semantics. This is the ownership model we want!

However, plainly using std::unique_ptr<Command> had some drawbacks. Primarily, implementing decorators would be impossible: unique_ptr is defined in the standard library so we can’t define methods on it, and any methods defined on Command wouldn’t have access to the owning unique_ptr.

The solution is CommandPtr: a move-only value class wrapping unique_ptr, that we can define methods on.

Commands should be passed around as CommandPtr, using std::move. All decorators, including those not supported in C++ before, are defined on CommandPtr with rvalue-this. The use of rvalues, move-only semantics, and clear ownership makes it very easy to avoid mistakes such as adding the same command instance to more than one command composition.

In addition to decorators, CommandPtr instances also define utility methods such as Schedule(), IsScheduled(). CommandPtr instances can be used in nearly almost every way command objects can be used in Java: they can be moved into trigger bindings, default commands, and so on. For the few things that require a Command* (such as non-owning trigger bindings), a raw pointer to the owned command can be retrieved using get().

There are multiple ways to get a CommandPtr instance:

  • CommandPtr-returning factories are present in the frc2::cmd namespace in the Commands.h header for almost all command types. For multi-command compositions, there is a vector-taking overload as well as a variadic-templated overload for multiple CommandPtr instances.

  • All decorators, including those defined on Command, return CommandPtr. This has allowed defining almost all decorators on Command, so a decorator chain can start from a Command.

  • A ToPtr() method has been added to the CRTP, akin to TransferOwnership. This is useful especially for user-defined command classes, as well as other command classes that don’t have factories.

For instance, consider the following from the HatchbotInlined example project:

33frc2::CommandPtr autos::ComplexAuto(DriveSubsystem* drive,
34                                    HatchSubsystem* hatch) {
35  return frc2::cmd::Sequence(
36      // Drive forward the specified distance
37      frc2::FunctionalCommand(
38          // Reset encoders on command start
39          [drive] { drive->ResetEncoders(); },
40          // Drive forward while the command is executing
41          [drive] { drive->ArcadeDrive(kAutoDriveSpeed, 0); },
42          // Stop driving at the end of the command
43          [drive](bool interrupted) { drive->ArcadeDrive(0, 0); },
44          // End the command when the robot's driven distance exceeds the
45          // desired value
46          [drive] {
47            return drive->GetAverageEncoderDistance() >=
48                   kAutoDriveDistanceInches;
49          },
50          // Requires the drive subsystem
51          {drive})
52          .ToPtr(),
53      // Release the hatch
54      hatch->ReleaseHatchCommand(),
55      // Drive backward the specified distance
56      // Drive forward the specified distance
57      frc2::FunctionalCommand(
58          // Reset encoders on command start
59          [drive] { drive->ResetEncoders(); },
60          // Drive backward while the command is executing
61          [drive] { drive->ArcadeDrive(-kAutoDriveSpeed, 0); },
62          // Stop driving at the end of the command
63          [drive](bool interrupted) { drive->ArcadeDrive(0, 0); },
64          // End the command when the robot's driven distance exceeds the
65          // desired value
66          [drive] {
67            return drive->GetAverageEncoderDistance() <=
68                   kAutoBackupDistanceInches;
69          },
70          // Requires the drive subsystem
71          {drive})
72          .ToPtr());
73}

To avoid breakage, command compositions still use unique_ptr<Command>, so CommandPtr instances can be destructured into a unique_ptr<Command> using the Unwrap() rvalue-this method. For vectors, the static CommandPtr::UnwrapVector(vector<CommandPtr>) function exists.