LWのサイゼリヤ

ミラノ風ドリア300円

21/5/3 ポインタと確定記述、変数名と固有名のアナロジーについて

C言語ポインタ完全制覇

俺はエンジニアではないのでC言語を使う予定は特にない(最近は非エンジニア向け言語であるところのSQLVBAに詳しくなりつつある)。よってポインタを完全制覇したところで意味は無いのだが、むかし情報系の人間だった頃に意味もなく購入して家に積まれていたので売るために読んだ。
読み物としてかなり面白かった。技術オタク丸出しの解説は立場に一貫性があって地に足が付いており、平易な割に説得力がある。

 

派生としてのポインタ

俺はポインタについて「アドレスに型情報が乗ったもの」くらいの解像度でしか理解していなかったのだが(挙動の理解にはそれで必要十分な気はするが)、そもそも規格書ではポインタ型は以下のような記載で与えられているというのは目から鱗だった。

ポインタ型は、被参照型と呼ぶ関数型、オブジェクト型または不完全型から派生してもよい。ポインタ型は、被参照型の実体を参照するための値を持つオブジェクトを表す。被参照型Tから派生されるポインタ型は"Tへのポインタ"と呼ぶ。被参照型からポインタ型を構成することを"ポインタ型派生"と呼ぶ。派生型を構成する方法は、再帰的に運用できる。

長いので要点だけ切り出すと以下の三点になる。

  1. ポインタ型は被参照型から派生する。
  2. ポインタ型は被参照型の実体を参照するための値を持つオブジェクトを表す。
  3. 派生型を構成する方法は再帰的に運用できる。

この見方がポインタの理解に新しい光を当ててくれるのは、被参照型とポインタ型の関係という観点からポインタを語っていることにある(派生という概念自体が、元になるものと新たに生成されたものの参照関係を前提していることは言うまでもない)。
通常の理解ではポインタとはコンパイラが型情報を付与した「アドレス」のことであって、間接参照演算子でアドレスを手繰って元の変数に到達できるのは、結果的にそうなっているに過ぎない。というのも、このイメージではintなりcharなりが宣言された時点でそれを格納している座標が先にメモリ上にあって、ポインタは論理アドレス空間上を走ってそれを拾ってくる使い走りでしかないからだ。
しかし、ポインタをまずは「派生型」と解釈するならば原因と結果が逆転する。ポインタが元の変数から派生したものであるというのが第一義的機能であるならば、それを変数がメモリ上に占める領域云々の話は間接的にそれを実現する実装の話に過ぎない。実際どれだけ本気かはわからないが、筆者は「たいていの処理系では『ポインタ型の値』は、実際にはメモリのアドレスのことです」と述べてポインタ型の値がアドレスではない実装も不可能ではないことを示唆している。

f:id:saize_lw:20210503224443j:plain

一般的な「アドレスのイメージ」が空間と座標で捉えるポインタだとすれば、上記の規格書に記述されている「派生のイメージ」は関係と指示で捉えるポインタとして対比できる。これらは実際にはポインタ概念の両面ではあろうが、どちらかといえば「派生のイメージ」の方が現実的に取り回しが良いように思われる。
というのも、派生のイメージによって、すなわち指示として理解できるポインタの使い方とは、むしろ極々一般的なポインタの使い方だからだ。複雑なコードの中でリスト構造を作る際に「さてどんな風にメモリが使われているのかしらん」などと思いを馳せながらnextメンバを記述する人はそうそういないだろう。逆に「アドレスのイメージ」の方が理解しやすいポインタの使い方としては、領域破壊じみた裏技の使用やmallocを用いた動的なメモリの確保があるかもしれない。

 

ポインタと変数名

ポインタを「派生のイメージ」で捉えたときに非常に気になってくることが一つある。ポインタが何らかの変数から派生して間接的にその値を指示するためのものだとして、ポインタが生まれる以前から全く同じ機能を持つものが既に存在しているということについてだ。それとはすなわち「変数名」である。

例えば

int a = 5;
int *a_p = &a;

と書いたとき、5という値に到達する手段は二つある。
一つはaのポインタであるa_pから間接参照演算子を用いて「*a_p」と書くことだ。しかしあまりにも当たり前すぎることだが、当然ただ単に「a」と書くことによっても同じものが返ってくる。いずれも同じ値を返すものだとすれば、ポインタと変数名の違いは何なのか。

むろん、技術的に説明するならば答えはあまりにも自明である。一つの答え方としては、「変数名は最初にコードに書くだけのもの、コンパイルされた時点で消えて実行時のバイナリレベルには残らない」というものがある。よって動的なメモリ確保には適さず、実行時点では詳細がわからないサブルーチン内で変数を確保したいときにはいちいち異なる変数名を付けることはできない。だからmallocで確保した領域にポインタを与えて使い終わった時点でfreeとかいう手順を踏むことになるわけだ。

しかし俺が気にしているのは、元変数から派生して値を指示するものとしてポインタを解釈したとき、指示の様態において変数名とポインタが持つ機能がどう異なるのかということだ。このあたりで俺の関心は必ずしも情報科学の話ではないと先に言ってしまった方がいいのかもしれない。
ではそうしよう。変数名やポインタが行う指示を言語における指示とアナロジカルに捉えたときの、それらの振る舞いの違いとは何か?

 

自動生成されるポインタときょうだいの名前

まずは入口として変数名とポインタの違いを「自動生成を受け付けるか否か」という点に求めたい。これが最もわかりやすいのは配列の生成である。例えば5人分の生徒の点数を格納する必要があるとき、初心者は以下のようなコードを書くかもしれない。

int alice_score, alex_score, bob_score, jon_score, kate_score;

これがかなり最悪なコードであることは言うまでも無い。それぞれの変数を独立して与えてしまっているのでforで走査できないし、読みづらいし書きにくいしタイプミスも起こる。普通は以下のように配列を宣言して済ませるだろう(どうしても名前の情報が欲しければ二次元配列か構造体配列にせよ)。

int score[5];

このとき、score[0]~score[4]は一括りに自動生成される。つまり4人目のscoreはわざわざ宣言していないにも関わらず、score[3]によってそれを呼び出せる。score[3]は*(score+3)と同値であるから、結局のところポインタを経由して4人目のscoreに相当する値への指示を自動生成したわけだ。

この配列による自動生成は、戦前の家庭がよく子供の名前を機械的命名していたのと似ている。五人のきょうだいが上から順に一郎、二郎、三郎、四郎……と名付けられていたとき、どう考えても二郎以降は生まれるたびに頭を捻って名前を考えられたわけではないだろう。名前は規則によって自動生成されたものであるから、子供が命名されている現場を見ていなくても、五人子供がいるならば四郎と呼べば対応する子供がいることが最初からわかっている。

逆に、こうではない命名の仕方としては、命名規則を作らずにきょうだいの名前を一人一人考えて付けていくことがある。例えば良太、義男、恒弘……といった具合に名付けられているきょうだいの列がそれだ。こうした場合、子供が命名される現場を見ていなければ子供の名前を呼ぶことができない。一つ一つ変数名を宣言する場合、それぞれを綴りまで追跡しなければならないのと同じだ。

だが、ここからが本題なのだが、少なくとも日本語においては、必ずしもこの二つは明瞭に分かれるわけではない。むしろ一つの名前が持つ二つのアスペクトとして考える方が妥当である。
例えば、いくら一郎、二郎、三郎、四郎……という名前が自動的に生成されるとしても、それは日本語話者にとっては自明というだけだ。一、二、三、四……という漢字の列に関する知識が無ければこの生成は成功しない。この意味において、完全に機械的に生成されているかのように思われた名前には、「漢字」という島国ローカルで予測しがたいオリジナリティが隠れている。厚切りジェイソンにとっては三郎の次が四郎であることは自明ではない。
同様に、良太、義男、恒弘……という名前群もまた、完全に規則から独立して文脈無く与えられているわけでもない。親が名前を考えていたとき見ていたドラマに義男という俳優が出ていたのかもしれないし、親が友人に借金を踏み倒された直後で「子供には義に篤い男になってほしい」という思いが生まれたのかもしれない。要するにその背景には恐らく確実に何らかの社会的文脈があって、命名の現場を見ていなくても、義男という名前を規則的に推測することも不可能ではないのである。

つまり、どんな名前においても社会的文脈に応じて自動生成される側面と、その名前に固有の意図でもって生成される側面があるはずだ。これらはウェイトが異なっており、一郎、二郎、三郎、四郎……列の場合は自動生成される側面の方が強く、良太、義男、恒弘……列の場合は固有の意図で生成される側面の方が強いという表現が妥当だろう。
この二つの対比が、変数の値に対する「ポインタとしての側面」と「変数名としての側面」とパラレルに対応していることは言うまでも無い。ポインタは派生として自動生成され、変数名はプログラマに固有の意図で生成されるからだ。

このアナロジーをついでにmallocにおける領域確保に対応させておこう。mallocした領域に固有の変数名をあてがうことができずポインタを割り振ることは、例えば紛争中の村で生まれた赤ん坊に名前を付ける両親がもう死んでいて、それを拾った治安部隊が応急処置として適当な管理番号を割り当てる場合に似ている。赤ん坊には赤ん坊自身に固有の命名儀式が欠落しているため、社会的ポジションから辛うじて呼び名を得ることしかできないのである。

 

確定記述としてのポインタ、固有名としての変数

ポインタあるいは社会的文脈に応じて自動生成される名前には、その名前自体にその人のポジションに関する情報が含まれており、この意味でその対象を確定する記述、確定記述と言える(二郎という名前には二番目に生まれたという情報が含まれている)。
一方、変数名あるいはそれ自身に特有の意図をもって命名される名前は一度限りのオリジナルで固有のもの、固有名と言える(義男という名前には二番目に生まれたという情報は含まれていない)。
だが、この二つの区別は少なくとも人間の名前においてはスペクトラム上の分配に過ぎず、全ての名前を二分する区別というよりはそれぞれの名前について有りうる二つのアスペクトであるということは既に述べた通りである。大雑把に言って、ポインタ(確定記述)が出生者の社会的文脈を尊重した保守的な呼び名である一方で、変数名(固有名)は先天的な性質に縛られないリベラルな呼び名であるようなイメージを得ておけば十分だ。

情報科学言語哲学のアナロジーがなかなか面白いことになってきた。このままどこまで突き進めるか試してみよう。ここまでポインタは配列絡みについてだけ扱ってきたが、他にメジャーな使い方としては「リスト構造」や「参照渡し」がある。

まずはリスト構造について。ここは技術ブログではないので詳説は省くが、どの入門書にも書いてある典型的な例として、例えば連絡網を管理するためにある家庭を表す構造体を定義し、自身のポインタ型変数としてnextをメンバに含めるあれのことだ。
そしてこれも言うまでもないことだが、リスト構造においてポインタの使用が要請される理由は主に高速化のためである。要素が入れ替わる際、いちいち実体を全てメモリ上でコピーペーストしていると処理に時間がかかってしまう。その点、ポインタならばただ参照先を張り替えるだけで済むため相対的にオーバーヘッドが小さい。
この局面でもポインタを「社会文脈に依存した確定記述」という名前のアスペクトに対応させると、一貫したイメージが得られる。そうした名前の側面とは、要するに現実の複雑さを一定程度捨象した上で既存の文化的流れの中にその名前を乗せようとする営みである。それぞれが個々に異なるオリジナリティを持っているということを無視して、「三番目に生まれたから三郎」「男だから花子ではなく太郎」とか呼んでしまう方が楽なのは当たり前のことだ。いちいちオリジナルな固有名を呼んでいると参照情報が増えて呼び間違えるリスクも増えるので、社会文脈から見た限りでの確定記述として呼んだ方が迅速なのだ。

続いて、参照渡しについて。これも目に穴が開くほど見た典型的な例だが、C言語でswap(a, b)を実装する際にはポインタを経由しなければ実際には値を変更できないという例の話だ。また、C言語は関数一つにつき値を一つしか返せないので、実質的に複数の結果を返したいときに複数のポインタを渡してそこに中身を詰めてもらうという用途で使うこともある。
この話の本質は明らかにC言語が標準で値渡しをするという挙動にある(最初から参照渡しならこんなポインタの使い方はしなくて済むのだし、実際他の言語ではそうなっていることも多い)。ここでもアナロジーを働かせるならば、人間において値渡ししかできないというのは、名前を剥ぎ取ってその人それ自体のコピーを渡すということだ。この局面において固有名による記載は闇に葬られる。というのも、そもそも固有名とはその人の持つ社会的文脈から寸断されて無関係に与えられたものだからだ。固有名は自動生成できず含みを持たないから、それを一度無理矢理剥がしてしまえばもう二度と復旧できない。
しかし、完全に記憶を喪失した人間にも呼び掛ける道はまだ残っている。それは社会的文脈の元へと彼を復帰させることだ。彼自身を見ていても何もわからないとしても、彼が三番目の入院患者であることさえわかれば、それに対応した適当な名前を復活させて看護師の間で流通させられるだろう。つまり、aという名前が剥ぎ取られていたとしても、&aというポジションは未だに機能しているのだ。ポジティブに見れば記憶喪失の人間が何度でも社会へと復帰できるのかもしれないし、ネガティブに見ればいくら自由になりたくても文脈からは拘束されて強制的に引き戻されるのかもしれない。

 

ポインタとしての確定記述、変数名としての固有名

言語哲学的には、名前のポインタ的側面はフレーゲ&ラッセルの名前観、変数名的側面はクリプキの名前観に対応する。
このあたりは説明すると長くなるので興味のある人は過去の記事を読むか(→)自分で調べてもらうとして(→)、このアナロジー言語哲学サイドからスタートしても有効なのかを「同一性」と「必然性」の二点から考察したい(問題設定についても詳説は略する)。

同一性の問題とは要するに「ヘスペラスはフォスフォラスである」という例文をどう解釈するかという話である。

補足377:楠栞桜が引退したので「夜桜たまは楠栞桜である」が使えなくなってしまった。

確定記述として見るならば、ヘスペラスとフォスフォラスは異なる情報を含む異なる名前と見做すことが許される。何故なら、名前とは対応する対象に到達するための情報が記述として刻まれているものであって、ヘスペラスとフォスフォラスは異なる情報を含んでいるからだ。
これはポインタ表現においては異なるポインタが間接参照によって同じ値を指す(同じポインタを格納する)のが許容されることと対応している。例えば、int* p;とint* qを宣言し、pとqに同じアドレスを格納することは全く可能である。というのも、pとqはたまたま同じ値に対応しているだけで、それらはそれぞれに異なる文脈的な経緯でアドレスを保持しているからだ。

一方で、固有名として見るならば、ヘスペラスはフォスフォラスは同じ情報を含む同じ名前である。何故なら、名前とはある対象がそれに与えられている情報とは独立してオリジナルに持つ固有のものであるはずなのに、それが同じ対象を指してしまっている以上は同一の固有性を持つもの、つまり同じものであるからだ。
これは変数名表現において同じ実体を指す名前はあらゆる意味で同じでなければならず、そのために変数名の重複が許容されないことと対応している。int a;とint b;が実は同じ実体であるということは有り得ないのだ。それぞれを一度個別に命名してしまったらそれは絶対に異なるものでなければならず、同じ実体を指すものがあるとすればそれは自分自身しか存在しない。この挙動がポインタと真逆であることは言うまでも無い。

最後に、必然性についても駆け足でさらっておこう。クリプキの直接指示としての固有名においては同じ名前は必然的に同じ対象を指示する。すなわち、状況の異なる可能世界においても固有の同じ対象を指示する。一方、確定記述においては状況の異なる可能世界では当然記述の内容が異なるので、同じ対象を指示できない。
C言語における可能世界は何かと言えば、コードが実行されるごとに異なることができる環境のことだろう。同じコードをコピペして別のマシンで動かすのでもいいし、同じマシンで違う日に動かすのでもいいし、同じマシンで並列に動かすのでもいい。
このとき、変数名(固有名)はどの環境で実行されようが同じ実体を正しく追跡する。というのも、変数名は実体がどのアドレスにどうあるかとは無関係に、宣言された時点でその実体に固有のものとして縛り付けられた形で存在するからだ。ソクラテスが仮にソフィストだろうが羊飼いだろうがどの世界でもソクラテスと呼ばれるのと同様、実行される環境が東大のスパコンだろうが京大のスパコンだろうが変数名は同じものに紐づけられる。
一方で、ポインタ(確定記述)はそうはいかない。アドレス空間の組成は環境によってまちまちであり、違う環境で実行されたポインタが同じアドレスを保有していることはほとんどないからだ。ある系ではpが0xbfffdc37だったのに別の系では0xbfeedc24だったなどということはいくらでも起こるのであって、前者の値をメモしておいてそれを使い回そうなどというコーディング(?)は愚の骨頂どころの騒ぎではない。それはソクラテスソフィストである世界で彼の有様を綿密に絵に描き起こしたとして、そのスケッチはソクラテスが羊飼いである世界では彼を発見するのに全く機能しないのと同じことである。