ポインタ同士の引き算 — 差は「要素いくつ分か」
今日のゴール
p - q(ポインタ同士の引き算)は、この仕様がまだ意味を決めていない機能である。
本物の C の慣習にならい、要素いくつ分離れているかを返す意味を与えて実装する。
この回の位置づけ
| 項目 | 内容 |
|---|---|
| 必須の前提 | コマ12(malloc)まで。差の考え方そのものはポインタ演算までで足りるが、tests/count.c が malloc を、tests/use_result.c が struct と malloc を使う。完了条件のうち golden.py は完成した final/mycc.py を土台に fixed17 を回すので、やり切るにはコマ15 も要る |
| 推奨の前提 | — |
| 改変しない | mycc.py、scaffold/、ラッパー langcc.py(配布済み・完成品) |
| 編集する | ptrdiff.py |
| 完了条件 | check.py の全 Step が PASS になり、golden.py が「修正なしで落ち、修正ありで通る」ことと fixed17 全通を報告する |
| コマ数 | 1 |
| 仕様との関係 | 仕様の外側。language_spec.md は「ポインタ同士の減算…はない」と明示的に除外している。「要素いくつ分か」を返す意味はこの回で自分で決めて足す。判定基準どおりの L 系である |
| 備考 | 言語機能発展シリーズ。L2・L3 とは独立している。3本の中では最も小さい |
なぜ L 系なのか
S 系と L 系は、その回で作る振る舞いの「正解」がどこに書いてあるかで分かれる。 仕様が意味を決めている構文の、仕様と実装のずれを埋めるのが S 系。 仕様が対象外と決めた機能に、意味を自分で決めて足すのが L 系である (発展課題の索引の「S と L の判定基準」節)。
S1 は「仕様が C とわざと違えている && / || の意味論を、C の側へ寄せる回」、
S2 は「仕様が要求している 32 ビットの折り返しに実装のずれを合わせる回」で、
どちらも仕様の該当行から出発する S 系である。
この回はどちらとも違う。仕様はポインタ同士の減算・ポインタの関係比較を
「無い」と明示的に除外しているので、埋めるべきずれがそもそも存在しない。
これは意味論の付け替えでも実装のずれでもなく、
仕様が意図的に落とした機能を、意味を自分で決めて追加する回——
すなわち判定基準どおりの L 系である。
この回は 2026-08-01 に
S3_ptrdiffからL1_ptrdiffへ改名した。 以前は判定が L 系なのに ID がS3のままという食い違いがあり、それを解消した。 ラッパーも S ファミリのsemcc.pyから L ファミリのlangcc.pyに変わっている。
コマ9 でやり残したこと
コマ9 では、ポインタと整数の演算に要素サイズを掛けることを学んだ。
int *p;
p + 2; // 2 * sizeof(int) = 8 バイト進む
p - 1; // 4 バイト戻るいまの実装は、この「ポインタ ± 整数」を正しく処理している。 しかし C には、もう1つのポインタ演算がある。
int *p;
int *q;
q - p; // ポインタ同士の引き算これはアドレスの差ではなく、要素いくつ分離れているかを返す規則になっている。
int *a;
int *p;
int *q;
a = malloc(sizeof(int) * 10);
p = a;
q = &a[7];
q - p; // C の答え: 7(要素7個分)いまの実装は sub するだけなので、28(バイト差)を返してしまう。
これは S2(32 ビットの折り返し)のような「未定義動作(符号付きオーバーフロー)の範囲内で、
選んだ挙動からずれている実装の詳細」とも違う。
仕様書はそもそも p - q の意味を定めていない
(p - q はポインタ同士の減算として明示的に除外されている)。
何も足さずに Sub の既存コード生成へ流すと、たまたま sub 命令が
そのまま出てバイト差が返ってしまう——だがこれは「仕様違反」ではなく、
単に誰も意味を決めていないだけである。コマ9 のテストは a[i] と
*(p + 2) しか書いていないので、q - p を書いた人だけが
「なぜか7倍ずれる」現象に出会う。
なぜ「要素数」なのか
一貫性のためである。C のポインタ演算は、次の等式が成り立つように設計されている。
(p + n) - p == n
p + n が「n 要素進む」なら、その差は「n」でなければ辻褄が合わない。
p + 2 は 8 バイト進むが、(p + 2) - p は 2 になる。
この規則のおかげで、次のような書き方が自然にできる。
int my_strlen(char *s) {
char *p;
p = s;
while (*p != 0) { p = p + 1; }
return p - s; // 進んだ「文字数」がそのまま長さ
}char は1バイトなので、この場合はバイト差と要素数が一致する
(だから char* では誤りが表面化しない)。
int* や構造体ポインタで初めてずれる。
どう直すか
引き算のあとに、要素サイズで割る。
左辺を評価して退避
右辺を評価
sub a0, a1, a0 ← アドレスの差(バイト数)
li a1, 4 ← 要素サイズ
div a0, a0, a1 ← 要素数に直す
要素サイズが 1(char*)のときは、割り算を出す必要がない。
判定も要る。Sub ノードは3種類が同居している。
| 式 | 型 | 生成 |
|---|---|---|
p - q |
ポインタ − ポインタ | 今回追加: 引いてから割る |
p - 3 |
ポインタ − 整数 | 既存: 3 に要素サイズを掛けてから引く |
i - j |
整数 − 整数 | 既存: そのまま引く |
左右の型を cg._type_of_expr() で調べ、両方がポインタのときだけ新しい生成に回す。
結果の型も変わる。
p - 3 の型はポインタ(int*)だが、p - q の型は整数である。
そうでないと (q - p) + 1 がポインタ演算になり、1 ではなく 4 足されてしまう。
この型の修正はラッパー langcc.py の側で行っている(_type_of_expr の差し替え)。
本物の C では、この型に ptrdiff_t という名前が付いている。
実装
| Step | 実装対象 | 内容 |
|---|---|---|
| 1 | is_ptr_diff |
両辺がポインタの Sub かを判定する |
| 2 | gen_ptr_diff |
引いてから要素サイズで割る |
生成の骨格(退避 → 評価 → 復元 → 演算)は、コマ2 以来の二項演算パターンそのままである。
python3 langcc.py tests/count.c
python3 langcc.py tests/count.c | grep -A2 subテスト
python3 check.py # 判定と生成命令列を確認(未実装は SKIP)
python3 golden.py # 修正なしで落ち、修正ありで通ること + fixed17最後に回す fixed17 は、
ポインタ以外の引き算まで巻き込んで壊していないことを確かめるためのものである。
| テスト | 確認内容 |
|---|---|
count.c |
&a[7] - a が 7 になる(28 ではない) |
char_diff.c |
char* の差(要素サイズ 1)が壊れていない |
use_result.c |
差の結果が int として扱われる((q-p)*2、(q-p)+1) |
発展課題
- ポインタの関係比較:
p < qはこの仕様ではポインタの関係比較ごと 「無い」機能である。追加するなら、何を決める必要があるか (符号付き比較でよいか、==/!=とは扱いが違うか、など) - 負の差:
p - qでp < qのとき、結果は負になる。divは符号付き除算なので正しく動くはずだが、実際に確かめる - 要素数の持ち回り: この言語には配列型が無いので
sizeof(a) / sizeof(a[0])という定番の書き方はそもそも書けない。mallocした領域の要素数をどう持ち回ればよいか (たとえば個数を別変数で保持する)を考える - 異なる型のポインタの差:
int *p; char *s; p - s;は C ではコンパイルエラーである。 Q1 の型検査に「ポインタ同士の引き算は同じ型でなければならない」を追加する (※この課題は L1 のptrdiff.pyを実装している場合のみ意味を持つ)
コラム: なぜ div が要るのか。
要素サイズが 2 の冪(4 や 8)なら、割り算ではなく右シフト(srai)で済む。
除算命令は乗算より数倍遅いので、実際のコンパイラは
「サイズが 2 の冪ならシフト」という最適化を必ず行っている。
B1 のピープホール最適化の練習問題として、
li a1, 4 + div a0, a0, a1 を srai a0, a0, 2 に置き換える規則を書いてみるとよい。
(言語からシフト演算子 <</>> そのものは消えたが、srai は命令としては
最適化で普通に使われる——ここで言うシフトは言語機能ではなく命令選択の話である。)