Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHP8.5は「戻り値の無視」をどう見抜くのか 〜voidキャストの裏側〜

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for Shinya Saita Shinya Saita
September 27, 2026

PHP8.5は「戻り値の無視」をどう見抜くのか 〜voidキャストの裏側〜

PHP8.5から実装されたNoDiscardの実装試用の裏側に迫ります

Avatar for Shinya Saita

Shinya Saita

September 27, 2026

More Decks by Shinya Saita

Other Decks in Technology

Transcript

  1. 自己紹介 斉田 真也(さいた しんや) Webエンジニア 株式会社ラクスのバックエンド 個人では Markdownエディタ Bokuchi を

    開発中(OSS) 趣味: ものづくり全般 特技: カードマジック 各種アカウント X: @saita̲shinya GitHub: shinya Qiita: @ssaita 2
  2. 4

  3. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 PHP 8.4までの以下のソースコードの挙動 $deadline

    = new DateTimeImmutable('2026-08-20'); $deadline->modify('+7 day'); // 締切を1週間延ばした……つもり echo $deadline->format('Y-m-d'); 結果は 2026-08-20 のまま。 DateTimeImmutable は自分自身を書き換えないので、新しいオブジェクトを返す挙動。 2行目の戻り値を受け取り忘れるのは陥りやすいバグ PHP 8.4 までは……特にエラーも何も出ないし、検知もしない 斉田真也 / X: @saita̲shinya / GitHub: shinya 6
  4. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 PHP 8.5 にアップグレードすると・・・

    Warning: The return value of method DateTimeImmutable::modify() should either be used or intentionally ignored by casting it as (void), as DateTimeImmutable::modify() does not modify the object itself 「戻り値を使うか、 (void) を付けてわざと無視だとしてる宣言をしよう」 と警告をしてくる。 これは PHP 8.5 で新しく搭載された #[\NoDiscard] 属性にあたる。 RFC: marking̲return̲value̲as̲important((void) キャストも同 RFC で追加) #[\NoDiscard("as DateTimeImmutable::modify() does not modify the object itself")] public function modify(string $modifier): DateTimeImmutable {} ※ 標準の 関数にも採用済みだし、もちろん自作の関数・メソッドにも付けられる 斉田真也 / X: @saita̲shinya / GitHub: shinya 7
  5. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 じゃあどんな風に使うのか? #[\NoDiscard("Be sure

    to use the returned value.")] function returnFalse(): bool { return false; } returnFalse(); $ok = returnFalse(); (void) returnFalse(); // // // Warning が出る 使っているので出ない 「わざと捨てた」の意思表示。出ない #[\NoDiscard] … 関数・メソッドに付与。戻り値が捨てられたら E̲WARNING が出る (void) キャスト … 8.5 で追加された新しいキャスト。 警告を黙殺する公式な方法 当たり前ですが、マニュアルにも書かれています。 斉田真也 / X: @saita̲shinya / GitHub: shinya 8
  6. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 本題 PHPのエンジンはどうやって 「戻り値が捨てられた」ことを知るのか?

    A. 構文木(AST)に「捨てた」フラグが記録される B. 実行時に値の参照カウントを監視する C. もっとシンプルな何か 斉田真也 / X: @saita̲shinya / GitHub: shinya 9
  7. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 前提: PHP コードの一生

    文字列 ソースコード → トークン列 ① Lexer → AST ② Parser → OPCode ③ Compiler → 実行 ④ Zend VM PHP は 上記のフローを通ってZend VM が実行する(らしい) この OPCode、実は簡単に見ることが出来る php -d zend_extension=opcache -d opcache.enable_cli=1 \ -d opcache.opt_debug_level=0x10000 demo.php 斉田真也 / X: @saita̲shinya / GitHub: shinya 10
  8. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 OPCode ダンプの見方 $ok

    = validate(); これをダンプすると、この3命令になる: 0005 INIT_FCALL 0 80 string("validate") 0006 V3 = DO_FCALL_BY_NAME 0007 ASSIGN CV0($ok) V3 記号 ; 呼び出しの準備 ; 実行して、結果を V3 に入れる ; V3 を $ok に代入 意味 V3 テンポラリ変数(命令の結果の一時置き場) CV0($ok) コンパイル済み変数 = PHP の $ok V3 = の部分 結果オペランド(結果をどこに置くか) 斉田真也 / X: @saita̲shinya / GitHub: shinya 11
  9. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 実験: 3通りの「呼び方」を並べてみる validate();

    (void) validate(); $ok = validate(); // ① 捨てる // ② わざと捨てる // ③ 使う 0000 INIT_FCALL 0 80 string("validate") 0001 DO_FCALL_BY_NAME ; ① 結果オペランドが無い! 0002 INIT_FCALL 0 80 string("validate") 0003 V2 = DO_FCALL_BY_NAME ; ② 結果を V2 に受け取って…… 0004 FREE V2 ; 直後に捨てている! 0005 INIT_FCALL 0 80 string("validate") 0006 V3 = DO_FCALL_BY_NAME ; ③ 結果を V3 に受け取って…… 0007 ASSIGN CV0($ok) V3 ; $ok に代入 ①だけ Vほげ = が無い。②はなんと一度受け取ってから捨てている(詐欺やんけ) 斉田真也 / X: @saita̲shinya / GitHub: shinya 12
  10. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 「捨てた」というような情報自体は、どこにも無い エンジンに「戻り値が捨てられた」を表す専用データは存在しない あるのは、CALL

    命令の結果オペランドが UNUSED かどうか VM は実行時に「この命令、結果の置き場が無い」を見て警告を出す そして (void) 使ったキャストの中身は 結果を一度受け取って、即 FREE する命令列をわざと出させることで、 VM に「使った」と思わせる、ある意味事実だけ残すような形 答えは C. もっとシンプルな何か でした。 斉田真也 / X: @saita̲shinya / GitHub: shinya 13
  11. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 Cのソースコードで確認①: 捨て方どう実装しているか foo();

    と書くと、コンパイラの zend_do_free() が後始末を担当。 FREE 命令を出す……のではなく、CALL 命令の結果欄そのものを消す: /* Zend/zend_compile.c: zend_do_free() */ if (opline->result_type == IS_VAR && opline->result.var == op1->u.op.var) { ... SET_UNUSED(opline->result); /* 結果オペランドを UNUSED に */ } 直前に出した CALL 命令を振り返って、結果オペランドを UNUSED に書き換える 「捨てた」という現象は、命令の結果欄が空欄になっていること 斉田真也 / X: @saita̲shinya / GitHub: shinya 14
  12. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 Cのソースコードで確認②: (void) はどうなってるか

    (void) 専用のコンパイル関数がある。UNUSED 化を あえてせず、FREE を発行: /* Zend/zend_compile.c: zend_compile_void_cast() */ case IS_TMP_VAR: case IS_VAR: opline = zend_emit_op(NULL, ZEND_FREE, &expr_node, NULL); opline->extended_value = ZEND_FREE_VOID_CAST; break; 結果オペランドは残る(= VM から見れば「使った」となる)→ 警告が出ない ZEND_FREE_VOID_CAST の印は、後でオプティマイザが使う(→ 後述のオチ) ちなみに、 (void) は式ではなく「文」 文法レベルで文の先頭にしか書けないので $x = (void) f(); は構文エラーになる (Zend/zend̲language̲parser.y) 斉田真也 / X: @saita̲shinya / GitHub: shinya 15
  13. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 Cのソースコードで確認③: 警告を出す VM

    側 すべての関数呼び出しが通る VM ハンドラの冒頭(Zend/zend̲vm̲def.h): const uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD; if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED | no_discard))) { ... zend_nodiscard_function(fbc); /* ここで Warning を発報 */ } #[\NoDiscard] の実体は関数フラグの 1ビット RETURN_VALUE_USED() = 「結果オペランドが UNUSED でないか」を見るだけ Deprecated チェックと同じ1回のビット演算に相乗り → 普通の関数の追加コストはゼロ 警告文は、属性オブジェクトを実際に生成して $message を取り出して合成している 斉田真也 / X: @saita̲shinya / GitHub: shinya 16
  14. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 実は・・・opcache オプティマイザの「気配り」 さっきの3行に

    opcache の最適化をかけると( opt_debug_level=0x20000 ): ; 最適化前 0001 DO_FCALL_BY_NAME 0003 V2 = DO_FCALL_BY_NAME 0004 FREE V2 0006 V3 = DO_FCALL_BY_NAME 0007 ASSIGN CV0($ok) V3 ; 最適化後 0001 DO_FCALL_BY_NAME (②は呼び出しごと消滅!) ← ①は温存 0002 ASSIGN CV0($ok) bool(false) ↑ ③は定数畳み込み (void) した②は「捨てると宣言済み」なので、呼び出しごと削除してよい なかったことになる(!?) 警告対象の①は、消すと警告まで消えてしまうので、最適化を拒否して温存 ここが個人的に「ほぉーん」となった ※言語仕様が、ちゃんとオプティマイザで配慮されてるのは( ・ ・)イイ!! 斉田真也 / X: @saita̲shinya / GitHub: shinya 17
  15. 関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 まとめ 「戻り値を使ったか」の判定は、OPCode の結果オペランドが

    UNUSED か で判断 foo(); → コンパイラが CALL 命令の結果欄を消す( zend_do_free ) (void) foo(); → 受け取って即 FREE する命令を出し、VM に「使った」と思わせる VM は Deprecated チェックと相乗りのビット演算1回で警告を判定 opcache は (void) 済みの呼び出しは消し、警告対象は温存する php-src(Cのソースコード)は grep NODISCARD するとすぐに見つけられます。 興味があれば読んでみてください 斉田真也 / X: @saita̲shinya / GitHub: shinya 18