最終更新日時:
が更新

履歴 編集

整数からポインタへの変換とポインタの由来の扱いを規定 [P2434R5](C++29)

このページはC++29に採用された言語機能の変更を解説しています。

のちのC++規格でさらに変更される場合があるため関連項目を参照してください。

概要

C++26までの規格では、整数からポインタへの変換によって、どのオブジェクトを指す有効なポインタが得られるのか(ポインタの由来、provenance)が厳密には規定されていなかった。C++29では、C++の抽象マシンがもともと持つ「オブジェクトのアドレスは実行ごとに定まらない(非決定的である)」という性質を土台として、この動作が次のように規定される。

  • 整数からポインタへの変換では、その整数へ変換できるポインタ値が1つ以上存在する場合、そのうちプログラムの動作が定義されることになる値の中から、未規定に選択された1つが結果となる。そのような値が存在しない場合、動作は未定義である
  • この規則によって、実際に取得したことのあるアドレスを、演算によって確実に再現する操作は定義された動作となる。一方で、取得していないアドレスを推測してポインタを作る操作は、アドレスがたまたま一致する実行がありうるとしても、未定義動作である

int x = 0;
int y = 0;
std::uintptr_t p = (std::uintptr_t)&x;
std::uintptr_t q = (std::uintptr_t)&y;

// XOR演算による値の交換。アドレスがどのように選ばれた実行であっても、
// 最終的にqはxのアドレス、pはyのアドレスを確実に再現するため、
// 変換結果はそれぞれx、yを指す有効なポインタとなる
p ^= q;
q ^= p;
p ^= q;

*(int*)q = 1;  // OK: xへの書き込み
*(int*)p = 2;  // OK: yへの書き込み

*(int*)8675309 = 3;  // 未定義動作: 取得したことのないアドレスの推測

この規則は、std::memcpy()std::bit_cast()などのビットコピーによってポインタの値表現を復元した場合にも同様に適用される。

また、変換と並行して確保された記憶域を指すポインタ値が選択されることもありうる。これによって、指していた記憶域が解放され、別スレッドが同じアドレスの記憶域を確保し直すようなロックフリーアルゴリズム(いわゆるpointer zap問題の典型例)を、整数への往復変換を用いて定義された動作として書けるようになる。

仕様

  • 整数型・列挙型からポインタ型への明示的な変換は、以下のように規定される
    • その値が、1つ以上のポインタ値を整数型へ変換して得られる値である場合、結果は「プログラムの動作が定義されることになる、そのようなポインタ値すべての中からの未規定の選択」となる。そのような値が存在しなければ、動作は未定義である
    • そうでない場合、結果は処理系定義である
  • トリビアルコピー可能型について、次の規定が整備される(このページに関係する範囲の要約)
    • 各型は処理系定義の値の集合を持ち、オブジェクトの各値表現は、その集合の互いに素な部分集合に対応する。オブジェクトポインタ型以外のスカラ型では、各部分集合が含む値は高々1つである(ポインタ型では、1つの値表現が、生存期間の重ならない複数のオブジェクトを指す値を含みうる)
    • ビットコピーなどの操作によってオブジェクトが値表現を取得(acquire)すると、オブジェクトの値は、対応する部分集合のうちプログラムの動作が定義されることになる未規定のメンバへ置き換えられる
  • ある評価がポインタ値を生成し、その評価が、指される領域の記憶域の生存期間の開始よりも前に発生する(happens before)場合、動作は未定義である
    • 変換と並行して確保された記憶域を指すポインタ値は選択されうるが、その評価より確実に後に確保される記憶域を指すポインタは作れない
  • 任意の2つのポインタ値の等値比較が、一貫した結果を返すことが規定される

#include <cstdint>
#include <iostream>

int main()
{
  int x = 0;
  int y = 0;

  // ポインタを整数へ変換し、XOR演算によって2つのアドレスを入れ替える。
  // どのようなアドレスが選ばれた実行であっても、qはxのアドレス、pはyの
  // アドレスを確実に再現するため、この操作の動作は定義される
  std::uintptr_t p = (std::uintptr_t)&x;
  std::uintptr_t q = (std::uintptr_t)&y;
  p ^= q;
  q ^= p;
  p ^= q;

  *(int*)q = 1;  // xへの書き込み
  *(int*)p = 2;  // yへの書き込み

  std::cout << x << y << std::endl;
}

出力

12

この機能が必要になった背景・経緯

ポインタと整数の相互変換の意味論としては、C標準の検討で提案されたPNVI-ae-udiモデル(記憶域の「露出 (exposure)」に基づくモデル)などが知られていた。しかし露出に基づくモデルには、std::memcpy()の代わりに明示的なコピーループを書くだけで記憶域が露出して最適化が阻害される、データ依存のない操作の順序に結果が依存するなど、恣意的に見える点があった。

本提案は、C++の抽象マシンでは「アドレスの選択は実行ごとに定まらない」ことを利用して、露出という概念を導入せずに同等の意味論を実現した。整数からポインタへの変換で「動作が定義される値が選択される」という規定は、暗黙的なオブジェクト生成がオブジェクトを選択する方式と同じ(都合のよい非決定性、angelic nondeterminism)である。

あわせて、記憶域が解放されて同じアドレスに確保し直されることでポインタが無効化される「pointer zap」問題のうち、整数への往復変換を使うロックフリーアルゴリズムのユースケースが、定義された動作として書けるようになった。

関連項目

参照