👈

"C++"では&buf[sizeof(buf)]はUBだったかもしれない

に公開

はじめに

以前、下記のようなコードをC++で書いた[1]

constexpr size_t N = 5U;
int buf[N];
for (int* p = &buf[0]; p != &buf[N]; p++) {
  do_stuff(*p);
}

そこで、ふと思った。

&buf[N]って合法なのだろうか?

配列アクセスa[i]*(a + i)の糖衣構文であり[2][3]、従って&buf[N]&(*(buf + N))を意味する。

&buf[N] <=> &(*(buf + N)) // Is this legal???

とすれば、下記のような評価が行われることになりそうだ。

  1. buf + Nの結果、配列の最後の次のアドレス(past-the-end pointer)を得る
  2. その場所を逆参照する(UB?????)
  3. その場所のアドレスを返す

1.は合法だが[4][5]、問題は2.である。2.だけならオブジェクトが存在しないので存在しない場所の逆参照となりUBだが、結局3.で1.と同じアドレスを得るわけだから打ち消しあって実は問題なかったりしない?なんなら、Cで書いてたときは問題なかった気がするんだけど。

と、そんなわけでご本尊(C++国際標準規格書)に何が書いてあるのか確認した訳である。実際に自分が参照に用いたC++のバージョンはC++14と少々古めだったのだが、事情は変わらなかったので最新のC++23で確認する。

C++23が言うには…

以下が、C++23の規格書の最終ドラフト(n4950)である。

https://timsong-cpp.github.io/cppwp/n4950/

しかしながら、よく読み込んでもこの件に関して何か特別な記載は見当たらない。Cとの互換性に関して記載された"Annex C C.6 C++ and ISO C"を参照しても、特にそれっぽい記載が見当たらない。じゃあ、Cの規格書も参照してみよう。

C17が言うには…

C++23が"Nomative reference"と言っている[6]C17の規格書の最終ドラフト(n2310)を参照してみる。

https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2310.pdf

すると、「6.5.3.2 Address and indirection operator」にこう記載されている。

The unary & operator ... If the operand is the result of a unary * operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted, ... Similarly, if the operand is the result of a [] operator, neither the & operator nor the unary * that is implied by the [] is evaluated and the result is as if the & operator were removed and the [] operator were changed to a + operator.

(和訳)
単項演算子&のオペランドが単項演算子*の結果であれば、*&のどちらの演算子も評価されず、その結果はどちらも取り除かれたものとas-ifで同じになる。同様に、オペランドが[]演算子の結果であるなら、&演算子も[]から暗示される単項の*のどちらも評価されず、その結果は&演算子が取り除かれて[]+演算子に変わったものとas-ifで同じになる。

これこれ!この情報が欲しかったんだよー。

// In C ...
&(*p) <=> p     // 同じ
&a[i] <=> a + i // 同じ

このルールであれば、元々懸念していた逆参照は起きないので、&buf[N]は問題ないわけである。

Stack Overflowが言うには…

同じ質問をしている人を見つけた。

https://stackoverflow.com/questions/988158/take-the-address-of-a-one-past-the-end-array-element-via-subscript-legal-by-the

Best answerは"legal"だと言っている。なんか、コメントでは色々と論争が続いてるっぽいけど、さすがにUBならC++の互換性の記載で書くでしょ。…たぶん。

と曖昧ながらも、暗黙的に合法なんだろうと結論付けようとした。

と思ったら…

こんなCWG Issueが、しかも結構最近で2024年。(こんなベーシックなことなのに?)

https://wg21.cmeerw.net/cwg/issue2875

どうやら、「これはUBと明記しよう!」とCWG(Core working group)が提案しつつも、「いやいや待ってくれ。これ合法だったことにしといたほうがよくね?」と合法化する流れになっているっぽい。C++26では合法になるってことでいいですか?

というわけで、Cでは合法なのにC++では今のところグレーっぽいということだった。これでCとの互換性を謳うの酷くない?そんなことを思った一日でした。

脚注
  1. buf + sizeof(buf)std::end(buf)などで良いのだが、諸事情によりこう書きたかった。 ↩︎

  2. https://timsong-cpp.github.io/cppwp/n4950/expr.sub#2 ↩︎

  3. 従って、不自然ではあるがi[a]でもa[i]と同じ結果が得られる。 ↩︎

  4. https://timsong-cpp.github.io/cppwp/n4950/expr.add#4.2 ↩︎

  5. https://timsong-cpp.github.io/cppwp/n4950/basic.compound#3.2 ↩︎

  6. https://timsong-cpp.github.io/cppwp/n4950/intro.refs#1.3 ↩︎

GitHubで編集を提案

Discussion