ARLISS Blog




2019/08/27

C++17/20が恋しい日々

Siriusチーム リーダー兼ソフト班の田所です。

私達のローバーにはRaspberry Pi Zero-Wを搭載していて、プログラムは全てC++で書かれています。

C++は好きな言語の1つで、プログラムを組むことは苦ではないのですが…
唯一の問題点は、

最新のC++が使えない!!!

ことなのです。

Raspberry Piのaptリポジトリに上がっているGCCはバージョンが少し古く、C++14(2014年に策定されたC++標準規格)までしか使用できません。
自前でコンパイルするのは面倒な上に、ARLISSではいざとなったとき予備のSDカードを使用したりするのでaptコマンドで入るGCCでコンパイルできないと面倒事に繋がってしまうのです。

C++14より後の機能が使えなくて何が問題かと言うと、例えば…

for (const auto & help : helps)
{
    // ヘルプを表すtupleを分解
    const auto & commandName = std::get<0>(help);
    const auto & args = std::get<1>(help);
    const auto & description = std::get<2>(help);

    // ヘルプを表示
    ...
}

std::vector<std::tuple<std::string, std::vector<std::string>, std::string>>型のhelps変数をrange-based forで回したいとき、タプルを分解するのが非常に面倒です。
かと言って、展開せずに使うと後のコードがstd::get地獄になって非常に読みにくくなってしまいます…。

C++11から使えるstd::tieを使うともう少しスッキリ書けるかも知れませんが、宣言と代入を同時に書けないので行数が減らない&不定値が不安になる上に、かつ参照型にもできないので余計なコピーが増えてしまいます。
また、std::tieでは型推論(auto)も使えません。

これは本来、C++17からの機能である構造化束縛(structured bindings)を使用して、以下のようにスッキリ書けるはずなのです。

for (const auto & [commandName, args, description] : helps)
{
    // ヘルプを表示
    ...
}

また、来るC++20では、コルーチンという便利な新機能が実装される予定です。
ローバー開発においては、このコルーチンという機能が使えるととても楽に制御プログラムを書くことができるようになりそうです。

現状、私達のローバープログラムでは、モーターやセンサーなどのデバイス系クラス、ナビゲーションやゴール検知などの動作を担うシーケンス系クラスなどにそれぞれ実装されたupdate関数が、毎フレーム順番に呼ばれるような設計になっています(いわゆるタスクシステム)。
これにより、センサ値取得やログ出力、ローバー制御などの複数のタスクをあたかも並列で実行しているような動きを実現しています。

このとき問題となるのが、ループ中に時間待ちを書くことができなくなることです。
ゲームなどを開発したことのある人はよく分かるかと思いますが、メインループ中に時間待ちの関数を書くとその間他の動作が行われなくなってしまいます。
(pthreadやstd::future/std::threadを使えば解決はできますが、一部でlocaltime関数などのスレッドセーフでない関数がさり気なく使われているので並列実行は使用したくありません。)

例えば、以下のようなプログラムは書いてはいけません。

void DummySequence::update()
{
    m_rover->setMotorPower(1.0); // 前進
    delay(1000); // 1秒待つ
    m_rover->setMotorPower(0.0); // 停止
    delay(1000); // 1秒待つ
    m_rover->setMotorPower(1.0); // 前進
    delay(1000); // 1秒待つ
    m_rover->setMotorPower(0.0); // 停止
}

update関数は短時間でreturnできないといけません。
そこで代わりに、以下のようなプログラムを書くことになります。
(ここで、Timeはtime関数やtime_t型の計算をラップした名前空間です。)

void DummySequence::update()
{
    double dt = Time::dt(Time::get(), m_startTime);
    if (dt > 3.0)
    {
        m_rover->setMotorPower(0.0); // 停止
    }
    else if (dt > 2.0)
    {
        m_rover->setMotorPower(1.0); // 前進
    }
    else if (dt > 1.0)
    {
        m_rover->setMotorPower(0.0); // 停止
    }
    else
    {
        m_rover->setMotorPower(1.0); // 前進
    }
}

このように、下から逆向きに書かないと、条件式を増やさないといけなくなってしまいます。

幸い、ローバープログラムではストップウォッチの機能を果たすTimerクラスというものが用意してあるので、代わりに以下のように書けます。

void DummySequence::update()
{
    if (!m_timer.elapsed(1.0)) // 0〜1秒
    {
        m_rover->setMotorPower(1.0); // 前進
    }
    else if (!m_timer.elapsed(2.0)) // 1〜2秒
    {
        m_rover->setMotorPower(0.0); // 停止
    }
    else if (!m_timer.elapsed(3.0)) // 2〜3秒
    {
        m_rover->setMotorPower(1.0); // 前進
    }
    else if (!m_timer.elapsed(4.0)) // 3〜4秒
    {
        m_rover->setMotorPower(0.0); // 停止
    }
}

コードの順番が時系列順にはなりましたが、秒数を示すコメントがないと分かりづらいです。

ここでコルーチンの出番です。
C++20のコルーチンはまだ全然理解できていないので、Unityのコルーチン機能を真似た疑似コードで記述しています。ご了承ください。
(C++20で実際に使用される予定のキーワードはyieldではなくco_yieldです。)

my_coroutine DummySequence::run()
{
    m_rover->setMotorPower(1.0); // 前進
    yield WaitForSeconds(1.0); // 1秒待つ
    m_rover->setMotorPower(0.0); // 停止
    yield WaitForSeconds(1.0); // 1秒待つ
    m_rover->setMotorPower(1.0); // 前進
    yield WaitForSeconds(1.0); // 1秒待つ
    m_rover->setMotorPower(0.0); // 停止
}

こんな感じで、コルーチンを利用するとローバープログラムが簡単に書けると思っています。
(まあ、最新のC++の込み入った機能は全員が習得しないと保守管理できなくなってしまうので、C++20が出ても今のままなんでしょうけど…。また、並行性が絡んできてスレッドセーフじゃない関数を排除しないといけなくなりそうで、保守管理もできなさそう…。)

とりあえず、C++17/20が恋しい日々を過ごしています!!以上です!!
ARLISS本番まであと10日!!



2019/08/27 12:12:08