Chiharu の日記

絵描き C/C++ プログラマーの日記です。

Parallel force - パラレル・フォース 〜タスク登録処理

パラレル・フォースではスプライト関連処理として下記のクラスを用意しています。
(多分そのうちまた増えますが、現状はこんな感じ。

  • 外部公開クラス
    • ISprite: スプライトの基底クラス(インタフェース)
      • FillSprite: 単一色の塗りつぶしスプライト
      • ImageSprite: イメージ描画スプライト
      • TextSprite: テキスト描画スプライト
    • SpriteManager: スプライト管理
    • Screen: スクリーン管理
  • 内部管理クラス
    • ScreenDrawer: スクリーン分割領域の描画
    • SpriteResourceHolder: 非同期描画のためのリソース延命
    • FpsTimer: FPS タイマー

Screen クラスでは描画フレーム管理を行っており、フレーム単位のタスク処理を外部から登録することができます。

typedef std::function<const bool (Screen&)> ScreenTaskProc;
class Screen {
 void addTask(const std::int32_t iTaskID, ScreenTaskProc iTaskProc);
 void removeTask(const std::int32_t iTaskID);
};

登録すると FPS に合わせて登録したファンクタがコールバックされます。C++0x のおかげで、タスク登録時のコードの書き方がちょっと変わりました。C++0x 前後でクライアント コードを比較してみます。

// C++0x 前
enum { screenTask1, screenTask2 };
struct task1 {
 const bool operator(Screen& iScreen) const { /* do task 1. */ }
};
struct task2 {
 const bool operator(Screen& iScreen) const { /* do task 2. */ }
};
void testTask()
{
 aScreen->addTask(screenTask1, task1());
 aScreen->addTask(screenTask2, task2());
}

C++0x より前だと、ファンクタである task1, task2 というシンボルが出現し、これにより『タスク』それ自体が強調されており、本来表現したい『タスクを登録する』という処理があまり伝わってきません。
C++0x だと、これが下記のように変わります。

// C++0x
enum { screenTask1, screenTask2 };
void testTask()
{
 aScreen->addTask(screenTask1, [=](Screen& iScreen) -> const bool {
  /* do task 1. */
 });
 aScreen->addTask(screenTask2, [=](Screen& iScreen) -> const bool {
  /* do task 2. */
 });
}

C++0x だとラムダ式として無名ファンクタを定義できるので、ファンクタのシンボルが不要になり、『タスク』それ自体の強調が回避されます。まぁ、こまかいこと差っぴいても、すごくすっきりします。ラムダ式って素敵。
で、これだけでもすっきりするのですが、繰り返し登録するようなタスクは下記のように書くようにしています。

enum { screenTask1, screenTask2 };
void addTask1()
{
 aScreen->addTask(screenTask1, [=](Screen& iScreen) -> const bool {
  /* do task 1. */
 });
}
void addTask2()
{
 aScreen->addTask(screenTask2, [=](Screen& iScreen) -> const bool {
  /* do task 2. */
 });
}
void testTask()
{
 addTask1();
 addTask2();
}

これでタスク登録の表現が下記のように変わりました。

  • C++0x より前
    • 『タスク』を登録する
  • C++0x
    • 『タスクを登録する』

タスク登録の際、C++0x のおかげで本当に書きたい処理が書けるようになった気がします。考えすぎかな。

C++0x 〜auto に対する不満

以前の日記にも書きましたが、C++0x のauto は便利です。左辺値の型推論とでも言うんでしょうか。

// C++0x より前
std::vector<int>::iterator aIt = aVec.begin();

と書いていたものが、

// C++0x
auto aIt = aVec.begin();

で済むようになる。と。
これはすごく便利で、パラレル・フォースでは、上記のように左辺値の型を明示しない書き方でコードの大半が構成されています。
んー。で、タイトルにある不満ですが…。
RISC CPU の場合*1、整数演算の際、整数レジスタ長未満の型の計算で、C/C++ コンパイラが計算結果のビット長を合わせるために、演算処理の都度マスク処理を挿入してくれる、という素敵仕様があります。マスク処理が挿入されれば、当然オーバーヘッドが発生します。
これを回避する方法としては、演算対象が std::uint16_t だったとして、演算中は std::uint_fast16_t として計算して、演算後に std::uint16_t に戻す、というのが妥当です。

const std::uint16_t foo(const sd::uint16_t iVal)
{
 std::uint_fast16_t aVal = iVal;
 // aVal に対していろいろ計算
 return aVal;
}

上記のように本来の型名がコードの近くに存在する状況であればまだ書けるのですが、そうでない場合は、型の選択に手間が掛かります。

struct Image {
 const std::uint16_t getWidth() const;
};
const std::uint16_t foo(const Image& iImage)
{
 std::uint_fast16_t aVal = iImage.getWidth();
 // aVal に対していろいろ計算
 return aVal;
}

私、上記のケースで std::uint_fast16_t をさっと導き出せる自信がありません。下記のように書けたら最高なんですけれど、そういう機能は C++0x にはないのかな。

auto_fast aVal = iImage.getWidth(); // auto_fast = std::uint_fast16_t

*1:Intel CPU では異なるビット長の整数レジスタを持っているのであまり気にしなくてもよさそうですが