2014年4月17日木曜日

構造体のサイズに関する面倒な問題

Windows の API では、構造体にその構造体のサイズを表すメンバを含んで、呼び出し側で構造体のサイズを設定することにより、 将来構造体のメンバが増えても互換性が保てるようにしていることが多いということを前回書きました。

しかし、この方法は面倒な問題をはらんでいます。
例えば、以下のような構造体があったとします。

typedef struct PERSON_TAG {
  DWORD cbSize;
  int   nAge;
} PERSON;

これを利用する際に、先頭のメンバに構造体のサイズを代入するようにします。

PERSON person;
person.cbSize = sizeof(PERSON);
person.nAge   = 17;
SetPerson(&person);

新しい SDK でメンバが増えて、以下のようになったとします。

typedef struct PERSON_TAG {
  DWORD cbSize;
  int   nAge;
#if _WIN32_WINNT >= 0x0900
  int   nWeight;
#endif
} PERSON;

#define PERSON_V8_SIZE CCSIZEOF_STRUCT(PERSON, nAge)

先ほどのコードを新しい SDK を利用してそのままコンパイルすると、cbSize は新しいサイズになりますが、 新しいメンバ nWeight に関しては何も設定していないため、問題が起こる可能性があります。 また、過去のバージョンの OS で動作しなくなってしまいます。

それを避けるためには、_WIN32_WINNT を古いバージョンで定義すればいいわけですが、 しかし他の場所で新しい OS の機能も利用したいという場合、困ってしまいます。
そういう場合は、以下のように書き直すことになります。

person.cbSize = PERSON_V8_SIZE;

これでこの部分に関しては問題なくなるのですが、しかしどの構造体にメンバが追加されたかを調べて、 それを利用している部分を探して書き直して…、と一々やるのは面倒です。

Microsoft もこの辺りの問題を認識しているのでしょう、 問題の発生が多そうな構造体に関しては既存の構造体にメンバを追加するのではなく、 新しい構造体を定義してしまうということがされています。
例えば OSVERSIONINFO はメンバが追加されて OSVERSIONINFOEX に、MONITORINFOMONITORINFOEX に、という具合です。
しかし別の構造体にしてしまうと、API 関数に渡す時にキャストする必要が出てくるのが良くないですね。

2016/4/24追記

いつからか、C++ では MONITORINFOEX は MONITORINFO から派生するようになったため、C++ ではキャストの必要がなくなりました。
しかし、派生したことによってアグリゲートでなくなったため、以下のような初期化がエラーになってしまうという別の問題が発生しています。

// error C2440
MONITORINFOEX mi = {sizeof(MONITORINFOEX)};

追記終わり

私は、そもそも構造体のサイズを指定するのに sizeof を使わせるのが良くないと思います。
PERSON の例で言えば、PERSON_V8_SIZE をメンバが追加された段階で定義するのではなく、最初のバージョンで定義して、 sizeof(PERSON) ではなく PERSON_V8_SIZE を使うようにドキュメントに記述するようにします。
そうすれば、SDK を更新しても書き直す必要は無くなります。

構造体の互換性を保つ方法として、サイズを設定する以外に、どのメンバが有効かフラグで指定する、という方法もあります。
例えば以下のように定義して、

typedef struct PERSON_TAG {
  DWORD mask;
  int   nAge;
} PERSON;

#define PERSON_MASK_AGE 0x0001

以下のように利用します。

PERSON person;
person.mask = PERSON_MASK_AGE;
person.nAge = 17;
SetPerson(&person);

こちらの方法では、サイズを指定する場合のような問題は起こりません。
実際に Windows の CommCtrl.h では、こちらの方法が取られているものも多いです。
(しかし、サイズとフラグの両方を設定するようになっているものもあり、今一ちぐはぐなのですが)
この方法の欠点として、メンバが多い場合フラグの指定が長ったらしくなってしまうというのはあります。

0 件のコメント:

コメントを投稿