[C言語] 共用体とは何者なのか
はじめに
C言語を学ぶと、共用体が出てきますが、「学んだだけ」だった方は謎に思っている方も多いかと思います。
Geminiに聞いてみたら、
メモリを節約できる
異なるデータ型間でのビットレベルの操作が可能になる
ハードウェアレジスタへのアクセスなどの低レベルの操作に使用できる
「はあ…(???)」って感じです。実際私もそう学校などでは習っていて、よく分かりませんでした。
実際のところ「メモリ節約」などはそれぞれが全くの正解です。ただ、私はそれは「油揚げに湯通しを必ずする昔ながらの料理人の匠の業」のようなもので、実は凡人にとっての利点は異なってくると思っています。
というのも、私も実際にそういった類のコードを見るまで、「共用体って何が良いの?」ってことはよく分かりませんでした。
ということで、特にC言語を現在学ばれている・または学ばれただけ…で終わってしまっている方向けに、「共用体の意義って何?」ってことを私の解釈を書きたいと思います。これは私の考えにすぎず、共用体の持つ1つの側面だと思いますが、皆さんの理解促進につながれば幸いです。
共用体はある値の中に「意味のある値」が箱詰めされているルールがあるものを「扱いやすくする」ことができる
最初に結論ですが、私の解釈では、ある値の中に「意味のある値」が箱詰めされているルールがあるものを「扱いやすくする」ために、共用体の最大の意義があると考えています。
身近なもので分かりやすいのは「RGBのカラーコード表記」です。#bdd499
はbd
, d4
, 99
で赤、緑、青の意味を持つ「キマリ」になっています。誰かが、とは分かりませんが、誰かがそう決めて仕様になりました。
しかし、そんな「キマリ」があるからには、私達はそれを使って何かをしたいとき、「人間が扱いやすい仕組み」が必要になります。重要なのは「人間にとって」で、「機械」ではありません。
パーミッションの例
この共用体を説明するのに私が好む説明は「パーミッション」です(あくまで例えで実際のLinuxの実装の話ではないです)。
MacかLinuxをお使いなら、chmod
で774
って入れたことがあると思います。
これは774
で1つの意味を持ちますが、7
と7
と4
にユーザ・グループ・その他とそれぞれ意味を内包しています。
こういうヤツって、時には全部の値を取りたいし、時には一部の値のみ取りたいケースが多いはずです。
C/C++では「フラグとなる定数値」を元に「フラグをセットしたり外したり」という処理が頻繁に出てくる言語です。そんな分かりにくいのダメだよね、ということで色々な機能が後の言語に乗ってはいましたが、Cなどではこれが一般のプラクティスとなっています。
以下はUnixのパーミッションを示す疑似コードです。それぞれに実行、書き込み、読み取りの各種のフラグ値が貼られています。
#define S_IXOTH (00001)
#define S_IWOTH (00002)
#define S_IROTH (00004)
#define S_IXGRP (00010)
#define S_IWGRP (00020)
#define S_IRGRP (00040)
#define S_IXUSR (00100)
#define S_IWUSR (00200)
#define S_IRUSR (00400)
これは以下のように、|
演算子を使って、変数にパーミッションをセットすることができます。便利そうですよね。その是非はともかく、とりあえず昔の人は、少なくともそう考えました。
便利で機械にとっても効率的でクール。そうなるように誰かが決めました。
unsigned short perm = S_IRUSR | S_IWUSR | S_IXUSR // rwx
| S_IRGRP | S_IWGRP | S_IXGRP // rwx
| S_IROTH; // r--
// other: r-x
perm |= S_IXOTH;
では、これらから「ユーザ」や「グループ」の設定がどうなっているのか、取り出したいとしたらどうでしょう。
それは、例えば次のように求められます。
int user = (perm >> 6) & 0x7;
int group = (perm >> 3) & 0x7;
int other = perm & 0x7;
ビット演算に慣れた方以外はよくわからないと思います笑。私は鈍感なので全くすぐ出てきません。この結果は775
です。
「その場しのぎ」のときの演算では、こういったビット演算はC言語で通常よくやる手です。でも慣れた人でない限り、よくわからないと思います。
そんな時に出てくるのが共用体です。このようなパーミッションを擬似的に共用体に表すなら、次のように考えることができるでしょう。
typedef union Permission
{
unsigned short perm;
struct
{
unsigned short other : 3;
unsigned short group : 3;
unsigned short user : 3;
unsigned short _ : 7;
};
} Permission;
これは、共用体を使ってものごとを解釈したい時は、擬似的にこう書けます。
Permission makePermission(unsigned short perm)
{
return (Permission){.perm = perm};
};
Permission permission = makePermission(perm);
// 他人に実行権限を与える
permission.other = 5;
permission.perm |= S_IXOTH; // これも出来る
ああ!見て誰もが直感で分かるはずです。
値の取得・設定の操作が、途端に簡単になりました。これです。共用体を使う最大の理由(と私が考えるのは)。
1つの値でも意味を持つが、その中の複数も固有の意味を持つものを、あたかも構造体かのように扱えることが出来ていますね。
こうやって、「必ず決まっているキマリ」を解釈するためには、「共用体は便利」ということです。
おわりに
では、こんなものが必要になりやすいシチュエーションとは何なのでしょうか。
特に使われている場面で思いつきやすいのは、「IO部分のシグナルの送受信」かと思います。
データが流れるところには「誰かが決めたデータの構造」が何かしらあります。誰かが仕様を決めます。
そういったものを決められた「ルール」に基づいて、実際の機械やソフトウェアが「やりたいことを人間側が沢山実装できるようにする」ためには、「その都度のビット演算」より「人間が見て分かりやすい仕組み」がむしろ必要になってきます。なぜなら、私達はそれを使って色々な何かを沢山したいからです。例えば、信号に応じてロボットの制御モードを変えたり、ゲームの流れを変えたりだとか。
そんなところが共用体の一番の活躍どころなのだと私は考えています。
それと、もう1つ共用体になら出来る役割があります。それは「中身が違っても外面は同じ型にできること」です。高級言語では、インターフェースやジェネリクスによって、「Iナントカ型」を実装していれば、対応する関数引数に自分を渡せますが、その関心を関数ではなく構造体側に置き換えた発想と考えていただけると、末代の我々には分かりやすいのかもしれません。
これは例えばWin32APIにはINPUT構造体というものがあり、マウスとかデバイスとか、色々な種別の情報を単一のSendInput()
関数で送信できるようになっています。そういった設計どころで活躍します(これもトコロとしてはIO関連と言えますね)。
リンク先のMSのドキュメントを見ると分かりますが、構造体にはtype
という値でWindows側は何が来ているんだろうか、ということを判別したいと考えた作りをしています。これはタグ付きの共用体という手法で、出来るだけ正しく判別した型を取り出したいという共用体を使う場合の設計意図を示しています。
ただ、正直なところ、実際共用体は一般に好まれたヤツではありません(と私は思います)。
先程も謎の_
をつけましたが、こうやって上手く調整してフィールドの順番も守らないと(アラインメントと言います)変なアクセスを起こしてしまい、プログラムのバグに繋がります。今回は美談ばかりで言ってしまいますが、バイトオーダーを考慮して問題がない、という前提がなければ、この美談は成立しない、なんてことも、説明は省きますが頭の片隅に置いていただければ幸いです。繰り返しになりますが、「良くない」と思われた部分も多かったわけで、それが末代の色々な言語の機能になっていったと理解しています。
また、「もう解析されたもの」を扱う上では共用体はどちらかというと重要ではありません。
低レイヤを超えた上でのレイヤでは、私達のやりたいことの殆どは、「映像の一続きのフィルムから1コマを正確に切り取りする」ことではなく、その1コマの「画面」を作ることです。そこでは、「どこに何が配置されていた方が良いだろうか」ということが関心になるので、「きちんと箱に1つ1つのものを詰める」構造体やクラスを扱う方がごく自然だと思いますよね。機械も人間も、こういった状況では「整頓されていた」方が良いのが理だと思います。そういった感じで、高級言語ほどオモテの言語仕様では使われてないと思います。
Discussion