WisdomSoft - for your serial experiences.

7.3 構造体の型変換

構造体の値を別の値に直接変換することはできませんが、ある構造体へのポインタを、別の型へのポインタとして変換することができます。これを応用して、物理的な構造に互換性のある構造体へのポインタを変換して利用できます。

7.3.1 ポインタにキャストする

今回は、構造体へのポインタを使って型変換してしまうテクニックについて解説します。この手法は構造体の性質から考えれば、意味の異なる構造に値を変換する反則行為のようなものです。構造体の設計者は場合によってそのような使われ方を意図していない可能性もあり、誤って扱うとプログラムがクラッシュし、デバッグを複雑にする可能性があることは認識しなければなりません。

構造体は、異なる型の構造体に代入することはできず、型変換も認められません。例えば、次のようなコードはコンパイル時にエラー判定を受けます。

struct Point pt = { 10 , 100 };
struct Size sz = pt;

キャスト演算子を用いて sz = (struct Size)pt; と記述した場合も同じです。異なる構造体形は型に互換性がないため、代入は認められないのです。

そこで、ポインタの型について思い出してください。ポインタ型はコンパイラがポインタを間接参照する時にサイズを識別するものにすぎませんでした。構造体の場合も同様で、構造体へのポインタは、構造体の型(インスタンスのメモリサイズ)を決定するための情報にすぎません。次の構造体を見てください。

struct Point { int x , y; };
struct Size { int width , height; };

これら Point 構造体と Size 構造体は異なる型なので意味的な互換性はありません。しかし、メンバを見るとこれらの構造体は int 型のメンバを 2 つ保有するという共通点があります。これは、Point 構造体と Size 構造体のインスタンスに割り当てるメモリサイズが同一であることを表しています。32 ビットコンピュータの場合、これらの構造体のインスタンスは 8 バイトとなるでしょう。

この事実さえ分かってしまえば、構造体へのポインタを異なるポインタ型に変換してインスタンスを制御することができるようになります。8 バイトという実体は int 型の 2 つの要素を持つ配列として認識することもできれば、char 型の 8 つの要素を持つ配列として認識することもできます。当然、サイズが同じなので Point 型のインスタンスを Size 型のポインタとして扱うこともできるのです。

コード1
#include <stdio.h>

struct Point { int x , y; };
struct Size { int width , height; };

int main() {
	struct Point pt = { 400 , 300 };
	struct Size *psz = (struct Size *)&pt; 

	printf("&pt.x = %p : pt.y = %p\n" , &pt.x , &pt.y);
	printf("&psz->width = %p , &psz->height = %p\n" , &psz->width , &psz->height);
	printf("psz->width = %d : psz->height = %d\n" , psz->width , psz->height);
	return 0;
}
実行結果
コード1 実行結果

コード1は Point 型の pt 変数のアドレスを Size 型のポインタ psz 変数に代入しています。これらは論理的な型の互換性はありませんが、メモリに格納されている物理的な情報を制御するという点から考えれば問題はありません。Size 型も Point 型もメンバ名が異なるというだけで、実質的な構成は同じです。結果を見れば、これらが真実であることを確認できるでしょう。

実行時の状態によってアドレスは異なりますが、&pt.x と &psz->width のメモリアドレスが同一であるということが重要です。&pt.y と &psz->height が同じであることも言うまでもありません。psz 変数が指すインスタンスは Point 型であれ Size であれ、同じメモリサイズであるという事実に変わりはないのです。

この事実は、システムを開発する設計者に重大な要素となります。システムは規模が大きくなればなるほど、将来の拡張やバグの解消、より最適なプログラム(必要メモリの軽減や高速化、移植など)が要求されます。こうしたプログラムの拡張において構造体と関数の関係は、システムの設計(デザイン)に直接影響を与える存在なのです。だとすれば、構造体と関数の関係は柔軟なものでなければならず、システムを拡張した時に従来のプログラムコードを全て書き直して再コンパイルなければならないような設計は避けなければなりません。

システム開発者は、将来の拡張に備えた関数の実装方法として、例えばこのポインタによる構造体の制御を使うことができます。ポインタを使えばインスタンスに関係なく、メンバを操作するだけで目的の情報を取得、または設定することができ、システムが提供する関数の役割を履行することができるのです。

コード2
#include <stdio.h>

struct Color {
	char *name;
	int r , g , b;
};
struct ColorEx {
	char *name;
	int r , g , b , a;
};

void SetColor(struct Color *color) {
	printf(
		"%s r = %d : g = %d : b = %d\n" ,
		color->name , color->r , color->g , color->b
	);
}

int main() {
	struct Color color = { "Color" , 0xFF , 0 , 0 };
	struct ColorEx colorEx = { "ColorEx" , 0 , 0xFF , 0 , 0xA0 };

	SetColor(&color);
	SetColor((struct Color *)&colorEx);

	return 0;
}
実行結果
コード2 実行結果

コード2は、Color 構造体へのポインタを受け取る関数 SetColor() 関数を定義しています。この関数は Color 構造体のメンバの値を表示するだけですが、その動作は重要ではありません。

システムの都合上、これまでは色情報に対して Color 構造体を使ってきていたが、新たにアルファ値を追加した ColorEx を扱わなければならなくなったという場合を想定しましょう。コード2はこのような要求を解決する手段のヒントとなるはずです。このとき、新たに定義する ColorEx 構造体は従来使っていた Color 構造体とメモリ構造レベルで互換性を取るために、name、r、g、b、までのメンバをまったく同じ位置に宣言します。そして、追加する情報をその後から宣言するのです。コード2では追加情報である a メンバを末尾で宣言していることが確認できます。

先頭メンバからの構造が Color 構造体と同じため、ColorEx 構造体のメモリ構造は Color 構造体と互換性があると考えられます。この場合は従来使っていた Color 構造体へのポインタを受け取る関数に ColorEx 構造体のポインタを渡しても、何の問題も発生しないのです。なぜならば、従来使っていた Color 構造体へのポインタを受ける関数は、ColorEx 構造体の追加情報には元々興味がないからです。ColorEx 構造体も、Color 構造体を基盤としているため name、r、g、b メンバに 対して Color 構造体として操作されても不都合はないのです。

このように SetColor() 関数に ColorEx 構造体のポインタを渡しても不都合はありません。このような設計を基にしたシステムは、従来のコードを変更することなく ColorEx 構造体を新たに定義して、従来とは別のアルファ値を扱うより便利な関数などをシステムに追加することができるでしょう。コード2では colorEx 構造体変数を渡す時に明示的型変換を行っていますが、これはコンパイラから警告を受ける可能性があるためです。より柔軟に設計したいのであれば SetColor() 関数で受け取るポインタ型を void* にするという方法もあります。

さらに柔軟性を求めて、SetColor() 関数が将来拡張した ColorEx 構造体などの予期しない型を処理できるようにしたい場合、構造体にその構造体自身のサイズを格納するメンバを追加します。この手法は「7.9 型のサイズ」で証明します。