今日のメニュー:お料理用語:宣言と定義

今日のポイント

宣言と定義の混同は混乱の元なので使い方をハッキリさせときましょ。

 

用語:宣言(declaration)と定義(definition)

典型的な「宣言」の例として main の前にグローバルスコープで

class TypeA;
TypeA *aPtr; // OK
TypeA  a;    // NG (compile error)
void DoSomethingTo(TypeA a);

のように書いた場合を考える。最初の行は TypeA がプログラム中のどこかでその具体的な内容が「定義」されているクラスを表すキーワードであることを「宣言」しているが、その中身が何であるかは ここでは「定義」されていない。一方、2番目の行は TypeA 型のポインターを「宣言」している。この段階では TypeA 型の中身が何であるかは問題ではない。しかし、3行目は TypeA のインスタンスを作ろうとしているので、その中身(デフォルトコンストラクターが何をすべきか)を知らないと実行できない。その意味でこれは「宣言」であると同時に実行文であり、インスタンス a の「定義」を与えているとも言える。その意味では2行目もポインターのインスタンスを作り、それ自体の記憶領域を確保するという実行を伴っているのでポインターオブジェクトのインスタンスの「定義」でもある。これに対し、1行目は何ら実行を伴わない純粋の「宣言」である。同様に、4行目は関数の「プロトタイプ」を「宣言」しているだけであって、その中身(定義)は不問であり、実行を伴わない。ここで重要なのは関数の引数と戻り値の型である。これは C++ のようなオブジェクト指向言語で関数が多重定義される可能性がある場合、期待しない関数を呼ぶというような間違いをコンパイルの段階で未然に防ぐレシピーである。これが実行を伴わない純粋な「宣言」であるゆえに、a を省略して

void DoSomethingTo(TypeA);

と書くことが許される。

次に、同じことをクラスの定義の中でやってみる。

class TypeA;

class TypeB {
  private:
    TypeA *aPtr; // OK
    TypeA  a;    // NG (compile error)

  public:
    void DoSomethingTo(TypeA a);
};

ここでも TypeA *aPtr は許されるが TypeA a は許されない(メンバー関数の引数はこれがプロトタイプ「宣言」なので問題ない)。そこで

class TypeA {
  private:
    int fA;
};

class TypeB {
  private:
    TypeA *aPtr; // OK
    TypeA  a;    // Now OK

  public:
    void DoSomethingTo(TypeA a);
};

のようにクラス TypeA の「定義」を与えてやるとコンパイルに通るようになる。しかし、前の場合との重要な違いはこの場合の TypeA a は純粋に「宣言」であることである。これは、「定義」しているものがクラスの中身であって、そのインスタンスではないからである。データメンバーのインスタンスはクラスのインスタンスが作られる時(クラスのコンストラクターが呼ばれる時)に初めて作られる(「定義」される)からである。このことは後に static という特殊調味料を扱う時にいやと言うほど思い知ることになるであろう。

このように「定義」という言葉を使う際にクラスや関数の内容を「定義」しているのか、特定の型のオブジェクトのインスタンスを「定義」しているのか区別することが重要である。前者はオブジェクトのインスタンスの生成をともなわないが、後者はインスタンスの生成をともなうという重要な違いがあるからである。

「宣言」という言葉を使った場合には、一般にはインスタンスの生成などの命令の実行を必ずしも伴わないが、特定の型のオブジェクトのインスタンスを「宣言」した場合にはコンストラクターの実行が伴うことになる。C や FORTRAN のセンスだと変数の「宣言」は実行文ではないと考えるのが普通だが、C++ では、整数型や実数型などの基本型のインスタンスを「宣言」した場合でも、それらの記憶領域がスタックに確保されるという意味で、これら基本型のコンストラクターが呼ばれたと解釈した方が自然である。この解釈では、ユーザー定義のクラスのコンストラクターの初期化リスト(initializer list)にならべる基本型データメンバーの initializer を基本型オブジェクトの初期値を引数にとるコンストラクターと見なすことになるが、これは首尾一貫した見方である。

この例はまた、もしデータメンバーとして TypeA a を加える必要がないのならば、そのクラスの「定義」がこの時点では不要であることを意味している。一般にはこれらの定義はヘッダーを読み込むことによってなされるが、これは make の際、余分なファイル間の依存性をもちこんだり、#include の循環を引き起こす原因になりかねないので、「不要なものは書かない」の原則に従い、TypeA がクラスであることを純粋に「宣言」するだけにとどめるべきである。