C言語の構造体をめぐって

 2003年8月に、NetNewsのfj.comp.lang.cに投稿された <871xve8046.wl@anago2.mas.chi.its.hiroshima-cu.ac.jp> (Fri, 22 Aug 2003 19:20:25 +0900) からC言語の構造体に関する議論が始まった。 この議論では、構造体に関する興味ある知見が色々と出てきたので、 議論に参加した1人としての立場で、今後のためにまとめておこうと思う。 従って、この文書の多くの部分は、 上記記事から発展した議論を構成する各記事の引用あるいは言い換えである。 どの部分がどの記事に基づくものか逐一記載しないが、 各論者には深く感謝したい。

構造体の用途と仕様

 そもそもの議論の始まりは、 「C言語の構造体で並び順が保存される仕様になっているのは何故か」 という疑問であった。 並びの間に「詰めもの」(padding)が入るのは許容しているのに、 並び順だけは保証されているというのは中途半端ではないかというのである。

 そして、そこから始まった議論の中で、 構造体の存在意義(用途)は1つではないということが指摘された。即ち、

  1. まとめて“組”として扱われるべきデータを、ある程度抽象的に記述したもの
  2. 入出力(ファイルや通信など)の際の「レコード」を具体的に記述したもの
  3. メモリ上のレイアウトを具体的に記述したもの
である。

 純粋に抽象的な“組”として扱うのが目的であれば、 メモリ上のレイアウトなど、どうでも良い。 実際、PascalやJavaのような抽象的な記述を指向している言語は、 並び順を処理系が勝手に変えてしまうような実装が普通らしい。 C言語から派生したC++において、 (少なくとも文法上の見掛けとしては)構造体の概念を拡張する形で導入された 「クラス」の実装においても同様である。

 しかし、C言語はあくまで「高級アセンブラ」である。 具体的な実装にまで踏込んでプログラムで記述できねばならない。 特に入出力の際の「レコード」を扱おうとする場合、 その内容を識別するために、最も簡単かつ確実に弁別する手段である 「並び順による照合」を必要とすることが多い。 そのためには、基本的にはプログラマが書いた通りに実装することが必要である。 並び順は勿論のこと、「詰めもの」もプログラマの責任で 必要に応じて明示的に入れるべきものであろう。

 ところが、CPUによっては、適切に「詰めもの」を入れなければ、 そもそも正常に実行できなかったり、 実行できても処理速度が落ちるという場合がある。 そこで「とりあえず実行できるように」処理系が「詰めもの」を入れることを 許容することになったのである。

 そもそも、この選択が混乱の元になったと言えるかもしれない。 適切な「詰めもの」を明示的に入れなければコンパイルエラーになる という仕様でも良かったであろう。 しかし、とにかく歴史的事実として 「詰めもの許容」という仕様を選択し、 その結果、既存のプログラムの移植や、 他の処理系と遣り取りするデータの入出力に際して、 「詰めもの」のことで逐一頭を悩まさねばならなくなったのである。

 とはいえ、処理系が「どう並べ替えても良い」と 「詰めものをしても良い」とでは、 移植性を高めるために必要な労力が大幅に違うということは、 多くの論者から指摘された。 「詰めもの」だけの問題であれば、 例えば、n個のメンバを持つ構造体であれば、 n-1ヶ所の詰めものに頭を悩ませれば済む話である。 しかし、並び順を勝手に変えられてしまう仕様だったら、 どう記述しても思い通りの並び順にならないことも考えられる。 そうなったら、妙な技巧(共用体の悪用とか、 アドレスをキャストしてポインタアクセスするとか)を使って 並び順が保証されたデータ表現にリンクさせるなど、 変則的な書き方をするしか無くなってくるだろう。

 また、構造体が表現する情報の質が場合によって異なる状況に対応するために、 構造体の最後の要素を「不定長」にするという技法がよく用いられる。 この技法が成立するには、 当該要素が「最後」であることが保証されていなければならない。 並び順が保証されれば、このことは連動して保証される。

構造体の歴史的起源

 ところで、構造体の用途についての議論の中で、 歴史的起源はCOBOLにおける「ファイル入出力のレコード」であろう という指摘がなされ、これをめぐる議論が進められた。

 C言語の当初のターゲット機がPDP-11 (前身のB言語の当初ターゲット機はPDP-7)であったことはよく知られているが、 このPDP-11のアセンブラには、構造体に相当する概念として 「FRAME疑似命令」というものがあったらしい。 C言語の「高級アセンブラ」としての性格から言えば、この「FRAME疑似命令」や 他のアセンブラ言語に存在する同様なものを、高級言語に移すというのが、 構造体というものを案出した本質的な動機だったという推測も成立する。 とはいえ、(この推測が正しかったとしても)そのために全く新たな概念を 案出したというわけでは無さそうである。 元になった「既存の概念」らしきものは、確かに存在している。

 高級言語というものが初めて作られたのは、 1954年のFORTRANだと言われている。 その後、1950年代の間にalgolとCOBOLが開発されて、 初期の高級言語の「御三家」とも言うべき三者が揃った。

 この段階で、algol(後の改訂版と区別するために「algol 60」と呼ばれる)には 構造体に相当する概念は無かった。 FORTRANにはCOMMONブロックと呼ばれるものが早い段階で規格化され、 バイナリレベルでは外部構造体(C言語のextern struct)と同等に 扱われる実装が多いという意味では、構造体の起源と考えられる側面もある。 しかし、あくまで他のプログラム単位(モジュール)との データ共有のみを目的とする存在 (主に、引数でデータを引き渡すと処理速度が落ちるという問題に 対処する目的で利用された)であり、 多段構造も許容されないことからみて、 発想的には構造体とは異なるものと考えた方が良さそうである。 それに対して、COBOLには構造体に相当する概念が初期から存在した。 おそらくは、これが構造体という概念自体の起源ではないかと考えられる。

COBOLにおける構造体

 COBOLの規格では「構造体(structure)」という用語を用いていない。 その理由は、COBOLの構造体が「ファイル入出力の単位」という性格を 明確に主張する仕様になっていることに関連するであろう。 そもそも、COBOLは「磁気テープ上のデータを読み込んで、加工して、 別の磁気テープに書き出す」処理を行うという発想で設計されていて、 変数は入出力ファイルにリンクしているのが「フツー」であり、 そうではない「作業用一時変数」は継子扱いである。

 COBOL以外のほとんどの高級言語のファイル入出力では、 例えば出力の場合には、プログラム中で記述された変数領域のデータを、 プログラムでは直接見えない「入出力バッファ」領域に複写して (書式つき出力の場合には、書式に従った変換結果をバッファ領域に書込んで)、 その(直接見えない)データをファイルに引き渡すという手順であろう。

 ところが、COBOLでは「入出力バッファ」の中に、 当該ファイル専用の(プログラムから見える)変数を確保する という発想になっている。 (実際には、ファイルとの間にワンクッション置く実装が多いと思われるが、 言語の習得に際しては、そのことを意識しない方が解りやすい。) 書式つき出力にしても「出力バッファ内の変数」への 代入に際して書式に従った変換が行われる (つまりバッファ内の各変数の属性の1つとして“書式”がある) という発想である。

 そうすると、入出力単位である「レコード」は、 「内容は多種多様だが“組”としてまとまっているデータ群」になるから、 その中身は自ずから構造体の性格を有することになる。

 そのため、COBOLでは他の構造体の要素ではない「一番上のレベルの」構造体と、 入出力単位である「レコード」が同義語になってしまう。 なお、「一番上のレベル」に限らない、中間レベルも含めた構造体のことは、 COBOLでは集団項目(group item)と呼んでいる。

 ちなみに、Pascal(1968年)では 構造体に相当するものを「record型」と呼んでいる。 この呼称は、COBOLの構造体がファイル入出力における「record」の概念と 密接に結びついているところから来ているのではないかとの推測が成り立つが、 あくまで推測に過ぎない。

COBOLからPL/Iへ

 COBOLにおける構造体の概念が他の言語に波及する 仲立ちをしたと考えられるのが、1964年に開発されたPL/Iである。 構造体(structure)という用語自体も、 開発時期から考えてPL/I起源である可能性が高い。 初期には「PL/1」と表記したらしいが、 現在は「PL/I」と書いて、最後の「I」を「one」と発音する。 ローマ数字の「壱」だと考えられるが、 「IBMのI」という意味も込められていると言われている。

 元々PL/Iは、適用分野ごとにFORTRAN, algol, COBOLが各々利用されていたのを 統合しようという野心の元に開発されたと言われている。 実際、その仕様には、各々の言語で記述されていた各分野のプログラムが 容易に移行できるように工夫したと思われる形跡がある。 その一環として、PL/Iの構造体定義の文法は、 COBOLのものを基本的に踏襲したと考えられる。 COBOLからの重大な変更は、これを「入出力ファイル」の呪縛から解放し、 単に「構造のあるデータ群」としたことである。 そして、おそらくは、この変更に伴って 「構造体」という命名がなされたと推測される。

 ついでに言えば、PL/Iには「COBOL式の入出力」と 「FORTRAN式の入出力」を並存させようとした形跡が認められる。 FORTRANの入出力では、書式の有無に関わらず、 個々の入出力毎に定義される「入出力並び」がある。 レコードとは、入出力並びに並んだデータの集団であり、 「偶々その場に居合せたというだけの集団」という感覚である。 それに対して、COBOLのレコードは、 この世の始まりから終りまで(=プログラムの実行開始から終了まで) 一貫して命運を共にする「恒常的な社会集団(??)」である。

 この点、PL/Iでは書式なしのREAD/WRITE(RECORD入出力)では 「並び」ではなく「1個の変数(=構造体を想定)」を引数とするのに対して、 書式つきのGET/PUT(STREAM入出力)では引数は「並び」である。 即ち、「RECORD入出力=COBOL式」「STREAM入出力=FORTRAN式」 という意図が感じられる。 C言語でもfread/fwriteで入出力されるデータは「1個の引数」なのに対して、 fscanf/fprintfでは「不定数の引数」になるというのは 似たような発想だと言えるだろう。

C言語へのPL/Iやalgol 68の影響

 一般的には、C言語は、 algol 60→CPL(1963年)→BCPL(1967年)→B言語(1970年)→C言語(1972年) という系譜で形成されたものだとされている。 しかし、別稿でも述べているように、 この系譜は事実の記述として一面的であり、 特に構造体などのデータ構造に関する部分は、この系譜とは全く無関係である。

 C言語のデータ構造は、 B言語を母体としてC言語という言語を新たに構築するに際して 導入されたものである。そして、その全体的デザインに関して、 開発者はalgol 68に負うところが多いと述べている。

【参考文献】
Dennis M. Ritchie, The Development of the C Language, http://www.cs.bell-labs.com/who/dmr/chist.html

 algol 68には構造体の概念があり、 「構造体(structure)」という用語を用いている。 これがPL/I起源であるかどうかを決定付ける情報は今のところ得られていない。 ただ、algol 68が仕様を公募して審査採択して作られたらしいことから考えて、 実用的に使われていた先行言語であるPL/Iを 参考にしたであろうという推定は成り立つ。

 PL/Iの構造体定義が、COBOLを踏襲して構造レベルを数字で表現しているのに対し、 algol 68では,1レベルしか記述できない形式を入れ子構造にすることによって、 複数のレベルが存在する構造体を記述する。 これはPascalやC言語でも同様である。 algolの流れを組む言語であれば、これが自然な発想であり、 むしろPL/IがCOBOLの伝統的な記述に無理して合わせたといえるかもしれない。 というわけで、この部分については、C言語は PL/Iよりもむしろalgol 68の影響を受けていると考える方が素直だろう。

 しかし、構造体のメンバー変数を表現するのに、 例えば構造体変数“parent”の中のメンバ変数“member”を algol 68流の“member OF parent”を使わずに “parent.member”と表記するのは、PL/I起源と考えるのが素直であろう。


構造体変数を定義する記述の比較

整数が2バイトの環境だと、 おそらく5つとも同じ構造の構造体になると思いますが、保証はしません^_^; PL/IやCOBOLでは(ついでに言うと、FORTRAN 77/90でも) 「80文字の文字列変数」と「1文字の文字変数が80個集まった配列」は 別物(本例は前者)ですが、他の言語では区別がありません。 Pascalの「packed array」は前者、packedでない「array」は後者に バイナリレベルで対応することを意識したものですが、 言語上の概念的な対応ではありません。 また、文字列の部分を取り出す時の指標の値が言語によって1つズレますが、 あまり気にせずに、各言語で最も自然とされる流儀を採用しました。

C言語

struct{
  int maindata;
  struct{
    int subdata;
    char subtext[4];
  }substrc;
  char maintext[80];
}mainstrc;

algol 68

STRUCT (
  INT maindata,
  STRUCT (
    INT subdata,
    [1:4] CHAR subtext
  ) substrc,
  [1:80] CHAR maintext
) mainstrc;

Pascal

var mainstrc: record
  maindata: integer;
  substrc: record
    subdata: integer;
    subtext: packed array[1..4] of char
  end;
  maintext: packed array[1..80] of char
end;

PL/I

DECLARE 1 MAINSTRC,
  10 MAINDATA FIXED BIN(15),
  10 SUBSTRC,
    20 SUBDATA FIXED BIN(15),
    20 SUBTEXT CHAR(4),
  10 MAINTEXT CHAR(80);

COBOL

DATA DIVISION.
FILE SECTION.
FD TARGET-FILE LABEL RECORD OMITTED.
01 MAINSTRC.
  10 MAINDATA PIC S9(5) USAGE COMPUTATIONAL.
  10 SUBSTRC.
    20 SUBDATA PIC S9(5) USAGE COMPUTATIONAL.
    20 SUBTEXT PIC X(4).
  10 MAINTEXT PIC X(80).

PL/IやCOBOLでレベルを示す「1」「10」「20」は、 昇順になっていれば「1」「2」「3」でも何でも良い (但し、トップレベルは「1」でなければならない)のですが、 後で変更することを考慮して飛び飛びの番号にするのが普通です。 なお、いずれの言語についても、字下げ(段下げ=インデント)は、 人間にとって読みやすくするために親切で入れたもので、 全く字下げが無くてもプログラムとしては等価です (但し、古い環境のCOBOLでは、 この例で字下げが入れてある行については、 4桁以上字下げせねばならないことになっている。 この例では字下げが2段階になっているが、 この2段階の区別は不必要) 。 また、COBOL以外の4言語については、 改行位置も(語の途中で無い限り)自由に変えることができます (COBOLでは改行を増やすことはできるが、行を連結して減らすことは不可)
なお、algol 68については、<u7k4rgpec.fsf@anet.ne.jp> (Wed, 03 Sep 2003 00:51:07 +0900)で OOTANI TAKASHI <tksotn@anet.ne.jp>さんが示されたコードを 微修正して採用させていただきました。


2003年8月29日初稿/2006年11月2日最終改訂/2014年1月23日ホスト移転

戸田孝の雑学資料室へ戻る

Copyright © 2003 by TODA, Takashi