組み込みと相性がいい C++ テンプレート

2020年1月22日

C++ にはテンプレートという機能があります。なんとなく見た目がいびつで、長年 C だけでやってきた人を寄せ付けない雰囲気があります。

ですが実は、テンプレートは組み込み屋のために用意されたのかと思うぐらい、組み込みと相性がいいものです。テンプレートの真髄は、「コンパイル時の解決」にあります。物事をコンパイル時に解決してくれるものは、だいたい組み込み屋の味方です。

テンプレート引数にできるもの

テンプレートの引数に持ってくることができるのは、大別すると次の3種です。

1. 型

テンプレートの用途の90%以上がおそらくこれです。型はプリミティブ型・列挙型・構造体・クラスの何でも構いません。

template <class T>
template <typename T>

class と typename に違いはありません。また、引数名は慣習的に T と書かれますが、必ずしもそうである必要はありません。

2. 整数

整数もテンプレートの引数にすることができます。大抵は int か size_t のいずれかで使うことになるでしょう。

template <int N>
template <size_t N>

引数名は N としておくのが無難です。

3. ポインタまたは参照

最もマニアックな使い方です。この方法を使って、クラスや関数を、特定のインスタンスに束縛してコンパイルすることができます。

template <PortInput *PI>
template <PortOutput &PO>

プログラムの実行時間を 1μs でも切り詰めたいとき、この手法で高速化が実現できる場合がありますが、コードが汚れるので多用はお勧めしません。

実践例

実際にテンプレートを使うと有利な場面をいくつかご紹介します。

コンテナクラスのサイズを可変にする

たとえばプログラムの中で移動平均を取りたい場面がいくつかあり、しかもそれは整数と小数のどちらもあり得るとします。さっと思いつく実装は、だいたいこのようなものです。

template <class T>
class MovingAverage
{
private:
  T m_values[16];
};

ですが、これだと配列は想定し得るサイズの最大を用意しておかなければなりません。そこでもう一工夫して、このようにできます。

template <class T, size_t N>
class MovingAverage
{
private:
  T m_values[N];
};

この手法は、C++11 の標準ライブラリ std::array でも採り入れられています。

加減乗除をオーバーロードしたクラスを放り込む

ローパスフィルタの実装をクラスで実現することを考えます。省略して書きますが、だいたい次のような実装になるでしょう。

class LowPassFilter
{
public:
  double filter(double x)
  {
    return x * m_b0 + m_x1 * m_b1 + m_y1 * m_a1;
  }
};

このクラスにテンプレートを適用すると、どうなるでしょうか。

template <class T>
class LowPassFilter
{
public:
  T filter(const T &x)
  {
    return x * m_b0 + m_x1 * m_b1 + m_y1 * m_a1;
  }
};

このローパスフィルタの柔軟性は、単に float でも double でも使えるというレベルにとどまりません。次のように、「加算と乗算ができるクラスなら何でも」通せるのです。行列さえも。

class Matrix
{
public:
  Matrix operator+(const Matrix &) const;
  Matrix operator*(const Matrix &) const;
};

LowPassFilter<Matrix> g_lpf;

このようにテンプレートと演算子オーバーロードは、うまく噛み合わせると強力な武器になります。複数の型でアルゴリズムを共用することは、大幅にバグを抑制することにつながるからです。

コールバック関数の許容範囲を広げる

コールバックという手法は組み込みではおなじみですが、クラスのメンバ関数を与えることができないという弱点を抱えています。たとえば次のようなコードは、コンパイルが通りません。

void execute(void (*callback)())
{
  (*callback)();  // コールバック関数の呼び出し
}

class Observer
{
public:
  void notify() { /* 通知を受けたときの処理 */ }
  Observer() { execute(&notify); } // エラー! 型が合わない
};

ここにテンプレートを適用することで、受け入れ条件を少しだけ緩和することができます。

template <class T>
void execute(T callback)
{
  (*callback)();  // コールバック関数の呼び出し
}

class Observer
{
public:
  void operator()() { /* 通知を受けたときの処理 */ }
  Observer() { execute(this); } // OK
};

このコードは従来の機能を維持しながら、クラスのメンバ関数をも受け入れられるようになっています。このテクニックは「ファンクタ」として知られています。