最終更新日時(UTC):
が更新

履歴 編集

function
<utility>

std::observable_checkpoint(C++26)

namespace std {
  void observable_checkpoint() noexcept;
}

概要

観測可能チェックポイント (observable checkpoint) を設置する。

C++では未定義動作を含むプログラムに対して、コンパイラは未定義動作が「将来」発生することを根拠に、それより「前」の操作を削除・変更する最適化を行うことが許容される。これは「タイムトラベル最適化」と呼ばれる。

例として以下のコードでは、nullポインタが渡された場合にエラーメッセージを出力してからデリファレンスに到達する意図だが、コンパイラは++*p未定義動作にならないと仮定してpがnullでないと推論し、if(!p)の分岐ごと削除できる。その結果、エラーメッセージが出力されることなくクラッシュする可能性がある:

#include <cstdio>

void inc(int* p) {
  if (!p) std::fputs("Null!\n", stderr);  // (1) エラー出力(削除される可能性がある)
  ++*p;                                   // (2) pがnullなら未定義動作
}

std::observable_checkpoint()を呼び出すと、その時点で観測可能チェックポイントが設置される。観測可能チェックポイントより前に完了した操作の観測可能な動作は、その後に発生する未定義動作によって遡って無効化されないことが保証される。これにより、タイムトラベル最適化を抑止できる:

#include <cstdio>
#include <utility>

void inc(int* p) {
  if (!p) std::fputs("Null!\n", stderr);  // (1) エラー出力
  std::observable_checkpoint();           // ここまでの観測可能な動作を保護
  ++*p;                                   // (2) pがnullなら未定義動作
}

この場合、std::observable_checkpoint()によって(1)のエラー出力が保護され、(2)の未定義動作があっても遡って消去されない。

std::observable_checkpoint()の明示的な呼び出しに加え、以下の操作も暗黙的に観測可能チェックポイントを設置する:

  • C標準の入出力関数(std::printf()std::fwrite()など)のうち、ファイルへのデータ書き込みを行う関数呼び出しからの復帰
    • デフォルトのsync_with_stdio(true)の状態では、std::coutなどの標準ストリームはstdoutと同期しているため、これに該当する
  • std::ofstreamなどのファイルストリーム出力時のstd::basic_filebufのオーバーフロー操作(バッファの内容をファイルに書き出した時点)
  • Unicode出力時のstd::print() / std::println()の内部出力関数std::vprint_unicode()によるターミナルへの書き込み(<ostream>版および<print>版)
    • Unicode出力時はネイティブのOS API(WindowsでのWriteConsoleW()など)を直接使用しC標準の入出力を経由しないため、このルールで明示的にカバーしている
    • 非Unicode出力時はC標準I/O関数を経由するため、上記のC標準入出力関数のルールでカバーされる

上記の例ではstd::fputs()がC標準の入出力関数であるため、(1)の呼び出しの復帰が暗黙の観測可能チェックポイントとなり、"Null!\n"の出力は(2)の未定義動作によって遡って消去されない。

効果

観測可能チェックポイントを設置する。

例外

投げない

備考

  • std::observable_checkpoint()の呼び出し自体は観測可能な動作ではない。コンパイラはこの関数を組み込み関数 (intrinsic) として実装でき、最適化後にゼロ命令のコードを生成できる
  • volatileアクセスは暗黙の観測可能チェックポイントではない。volatileアクセスの順序保証は従来の happens before 規則に従う
  • この関数はフリースタンディング環境でも使用可能である

基本的な使い方

#include <cstdio>
#include <utility>

int table[10];

void process(int i) {
  std::printf("processing %d\n", i);  // (1) 暗黙の観測可能チェックポイント
  table[i] = i * i;                   // i が範囲外なら未定義動作
}

int main() {
  for (int i = 0; i < 12; ++i) {
    process(i);
  }
}

この例では、iが10以上になるとtable[i]で範囲外アクセスの未定義動作が発生する。しかしstd::printf()はC標準の入出力関数であり、呼び出しの復帰が暗黙の観測可能チェックポイントとなる。そのため、iが0から9までの反復で出力された"processing 0"から"processing 9"は、その後の未定義動作によって遡って消去されることはない。

出力例

processing 0
processing 1
processing 2
processing 3
processing 4
processing 5
processing 6
processing 7
processing 8
processing 9
processing 10
processing 11

明示的なチェックポイントの設置

#include <cstdio>
#include <utility>

int main() {
  int result = 0;

  for (int i = 1; i <= 5; ++i) {
    result += i;
  }
  std::printf("sum = %d\n", result);
  std::observable_checkpoint();  // ここまでの出力を保護

  // 以降のコードに未定義動作があっても
  // "sum = 15" の出力は保証される
  int* p = nullptr;
  *p = 0;  // 未定義動作
}

出力例

sum = 15

バージョン

言語

  • C++26

処理系

関連項目

参照