Wireless・のおと

クラスとテンプレートのはなし

ブログ
プログラミング 昔話

前回に続き、プログラミングの話です。今回は C++ の簡単なお話です。

単純に数を数えてみる

さて「数を数える」プログラムコードを考えてみましょう。一番最初に思いつくのは、グローバル変数を定義して必要なときに加算や参照することです。単純ですね。

int count;

func(...)
{
    :
    count++;
    :
    printf("%d", count);

数える対象が1つの時はこれでも良いのですが、複数になると count, count2, count3... という風に変数が増えてゆき、段々「どれが何を数えているんだっけ?」と混乱してきます。いまどきの言語なら interrupt_count とか receive_count とか「それが何を示しているか」を示す変数名を付けることができますが、古い FORTRAN とか COBOL の時代には変数に使える文字種や文字数が限られていたので、「CNT2 が何であるか」をソースコードと別のドキュメント(※註)に記すことが「良いプログラマー」の行いとされていました。

※註:いわゆる「変数仕様書」です。当時はこういうドキュメント類も手書きの紙の書類でした。それどころかソースコード自体も紙に鉛筆で書いて「パンチャー(何故か女性作業員が多かった)」に渡しマシンに「打ち込んでもらう」時代もありました。今ではそんな職場光景は想像すらしにくいかも知れませんね。
 

実はそんなに単純でもない

さて「単純」なはずのこのプログラム、場合によっては正常動作しません。数千回に一回カウントが「すっぽ抜ける」現象が起きたりします。これはマルチタスク(スレッド)システムでは顕著ですが、マルチタスクでなくとも割り込みハンドラの中で count 値を弄るようなことをすれば発生します。
count++ という演算はソース上では1行でも、実行コードレベルでは「アドレスから値を読み出す」「値を1つ加算する」「アドレスに値を書き戻す」という3段階から成り立っており(これを RMW:Read-Modify-Write 操作と呼びます)、「値を書き戻す」前に割り込みが入ると「書き戻すべき値が書き換えられており、そこに古い値を上書きしてしまう」ことが発生するからです。これをレース状態(Race Condition)と呼んだりします。

これを防ぐためには、count 値の操作前後を割り込み禁止に設定したりします(※註)。

    disable_interrupt();
    count++;
    enable_interrupt();

※註:大抵の OS には「割り込み禁止」よりも効率的な Mutex や Semaphore や Spinlock などといった機能が実装されていますが、詳細は割愛します。

さて問題は、ソースコード上でグローバル変数 count に対し RMW 操作を行う可能性のある場所全てに適切な割り込み操作を行わなければならないことです。count2 とか count3 とかが増えていた場合は、それらに対しても対策が必要です。全てのファイルを検索して全ての対応箇所に2行を挿入してゆくのは大変で、見落としとかコピペミスとかの「ケアレスミス」混入の余地もどんどん上がってゆきます。

構造化プログラミング

古典的な構造化プログラミング(Structured Programming)の考え方では、繰り返される定型操作は「サブルーチン」としてまとめることが推奨されます。C 言語の場合、これは「関数」という形で実装されます。たとえば

void countup(unsigned int *p_cnt)
{
    disable_interrupt();
    *p_cnt++;
    enable_interrupt();
}

という関数を定義し、単純な ++ 演算に代えて countup(&count); とか countup(&count2); という関数呼び出しに換えれば良いわけです。これには「お約束を1箇所にまとめてコードの見通しを良くする」とか、「移植時に弄る箇所が1箇所に集約される」といったメリットも一緒についてきます。

しかし構造化プログラムを用いても、カウンタの本体である count は剥き出しのグローバル変数のままです。プロジェクトを引き継いだプログラマーが「お約束」を知らずにうっかり count++; と書いて「数千回に一度のすっぽ抜け」を再発させてしまわないとも限りません。そして古典的な構造化プログラミングでは、そういう「うっかり」を防ぐ仕組みを作ることができません(可能だけれども無駄に複雑になったり、不自然なコーディングが必要になったりします)。

オブジェクト指向プログラミング

オブジェクト指向プログラミング(Object Oriented Programming)は、そこに枠組みを与えることができます。C++ ならば例えば

class counter {
private:
    unsigned int cnt;
public:
    counter(void) { cnt = 0; }
    ~counter() {}
    unsigned int get(void) { return cnt; }
    void set(unsigned int val) { cnt = val; }
    void up(void) {
        disable_interrupt();
        cnt++;
        enable_interrupt();
    }
};

という格好で「カウンタークラス」を定義します。これを使うときには

counter count;

func(...)
{
    :
    count.up();
    :
    printf("%d", count.get());

のような格好になります。
C++ のクラスは構造体の拡張であり、「オブジェクト名.メンバ名」でクラスの内部メンバにアクセスすることができます。メンバ cnt を public に置けば count.cnt; を直接操作可能になりますが、ここでは private に置いてあるので「外から」直接操作はできません。cnt 値を弄るためには set() あるいは up() の「メンバ関数」を経由しなければならず、RMW 操作の前後で割り込みが禁止されることは自動的に保証されます。これを「隠蔽」と呼んだりもします。

 

テンプレート

C++ のクラスは柔軟で強力な機構ですが、「型」に縛られる弱点があります。ここではカウンタ型として unsigned int を使いましたが、場合によってはマイナス値を扱う必要があったり、40 億を超える値が必要になったり、逆に1件あたりのカウント数が小さいものの件数が数百万に達するのでカウント1つあたりの占有メモリ量を減らしたい(short や char 型を使いたい)場合があるかも知れません。
こういった要求に答えるため、C++ では「テンプレート」という文法が拡張されました。テンプレートを使ったカウンタクラスは下記のようになります。

template <class T>
class counter {
private:
    T cnt;
public:
    counter(void) { cnt = 0; }
    ~counter() {}
    T get(void) { return cnt; }
    void set(T val) { cnt = val; }
    void up(void) {
        disable_interrupt();
        cnt++;
        enable_interrupt();
    }
};

やっている事は純粋クラス型と殆ど同じですが、カウンタ型の「unsigned int」に相当する部分が大文字の T になっています。この T にはクラスを使う側の都合で型を指定します。

counter<unsigned int> count;

func(...)
{
    :
    count.up();
    :
    printf("%d", count.get());

クラス名に続く <> の中身が「T のところに入れる型」です。ここを書き換えるだけで符号無し char だろうが符号付き int だろうが 64bit 整数だろうが、あるいは double 型だろうが string 型だろうが、その「型」がテンプレートで使われている演算子(ここでは参照、代入、加算の3つ)をサポートしている限り「何でも」渡すことができます。もっとも、それに実用的意味があるかどうかは別ですが。
テンプレートを使うことにより、ソースコードから「型」を排除して純粋なアルゴリズムの骨組みだけを実装することも可能です。これによって C++ でも Perl や JavaScript などの「型という概念が無い言語」の真似をすることもでき、これを「ジェネリック・プログラミング(Generic Programming)」と呼んだりもします。

C++ の純粋クラスは「構造体と関数のセット」であり、コンパイラを通すと「クラス構造体へのポインタを第一引数に持つ関数群」がオブジェクトとして生成されます。しかしテンプレートクラスでは呼び出し側で指定するまで型が未定なので、原則として事前コンパイルができません。つまりテンプレートの仕組みは関数よりも #define マクロに近いものになっています。いまどきのコンパイラではテンプレートクラスでもライブラリを作れるようにはなっていますが内部的にはかなり無茶なことをやっていて、その無茶はコンパイルエラーのメッセージやデバッガでのシンボル表示に現われます。画面を2周3周するほど長いエラーメッセージやデバッガでの引数表示を見るとゲッソリします。

まとめ

以上、「数を数える」という物凄く単純な操作を対象として、1970 年代 FORTRAN から C++ のテンプレートまで「以前はどうしていたのか」「それでどんな問題が起きて」「何故新しい言語が発明されたのか」を解説してみました。クラスには「継承(Inheritance)」とか「多様性(Polymorphism)」といった機能も重要ですが、今回はバッサリ割愛しています。そういう「オブジェクト指向ならでわの機能」を大上段に振りかぶって解説することは、「オブジェクト指向ってなんか難しい」と敬遠される原因になると思っています。
C++ 言語にはとりわけ、明示的 virtual 宣言だの演算子オーバーライドだのフレンドクラスだの多重継承だの static メンバだの、「出来ないよりは出来た方が良いだろう」的な機能がテンコ盛りなので、機能の解説から入ると「これ全部覚えなきゃいけないのかよ」と途方に暮れ「もう俺は一生 C でいいや」と投げ出してしまった人が少なくないんじゃないかと思います。1983 年に発表されて以来もう 30 年以上にもなるのに今一つ普及度の低い(特に組み込み分野では) C++ の、「オブジェクト指向って難しいんでしょ?」とか「C++ って動的メモリ管理(new/delete)必須なんだよね?」という誤解を解く一助になれば幸いです。

製品のご購入・サービスカスタマイズ・資料請求など
お気軽にお問い合わせください