C++のassertとは
C++のassertは、プログラムの中で「この条件は必ず成り立つはずだ」という前提を確認するための仕組みです。
主に開発中やデバッグ時に使われます。
条件が成り立たなかった場合にプログラムを停止させることで、不正な状態や設計上の誤りを早期に発見しやすくします。
厳密には、assertは関数ではなくマクロです。
見た目は関数のように使われますが、実際にはプリプロセッサによって処理されます。
assertの役割
プログラムの前提条件を確認する
assertは、プログラム中の前提条件を明示するために使います。
たとえば、ある処理が「値は必ず正である」「ポインタは無効ではない」「配列の添字は範囲内である」といった前提で動いている場合、その前提が崩れていないかを確認できます。
このような前提は、プログラムが正しく設計・実装されていれば成り立つはずのものです。
もし成り立たない場合は、呼び出し側や処理の流れにバグがある可能性があります。
バグを早期に発見する
assertの大きな目的は、バグを早い段階で見つけることです。
プログラムでは、不正な値が発生した直後には問題が表面化せず、しばらく処理が進んでから別の場所でエラーになることがあります。その場合、原因を探すのが難しくなります。
重要な箇所にassertを入れておくと、不正な状態になった時点でプログラムを止められます。
これにより、問題が発生した場所を特定しやすくなります。
コードの意図を明確にする
assertは、単にエラーを検出するだけではありません。
コードを読む人に対して、「この時点ではこの条件が成り立っている前提で処理している」という意図を伝える役割もあります。
条件分岐による通常のエラー処理とは異なり、assertは「この条件が崩れることは想定していない」という意味を持ちます。
そのため、関数の前提条件や内部状態の整合性を示すのに役立ちます。
assertが失敗したときの動作
条件が真の場合
assertに指定した条件が成り立つ場合、特別な処理は行われません。
プログラムはそのまま次の処理へ進みます。
正常な状態では、assertはプログラムの流れを変えません。
あくまで、想定外の状態になったときに問題を検出するための仕組みです。
条件が偽の場合
assertに指定した条件が成り立たない場合、通常は診断情報が出力され、プログラムは異常終了します。
出力される情報には、失敗した条件式、ファイル名、行番号、関数名などが含まれることがあります。
ただし、表示内容や形式は、コンパイラ、標準ライブラリ、OS、実行環境によって異なります。
重要なのは、assertが失敗したときは、通常のエラー処理のように回復を試みるのではなく、「プログラムにバグがある」と判断して処理を止める点です。
std::abortが呼ばれる
有効なassertが失敗すると、診断情報を出力したうえで、通常はstd::abortが呼び出されます。
std::abortは、プログラムを異常終了させるための処理です。
つまり、assertは「問題を通知して処理を続ける」ためのものではありません。
異常な状態を検出し、そこでプログラムを止めるための仕組みです。
NDEBUGとassertの関係
NDEBUGが定義されているとassertは無効になる
assertを理解するうえで重要なのが、NDEBUGというマクロです。
NDEBUGが定義されている状態でassertのヘッダを読み込むと、assertは無効になります。
無効化されたassertは、条件を確認しません。さらに、assertに渡された式も評価されません。
そのため、assertが無効化されると、実行時には何も行われないものとして扱われます。
ヘッダを読み込む時点が重要
NDEBUGの影響は、assertを定義するヘッダを読み込む時点で決まります。
ヘッダを読み込む時点でNDEBUGが定義されていれば、assertは無効になります。
一方、ヘッダを読み込んだ後にNDEBUGを定義しても、すでに定義されたassertには基本的に影響しません。
そのため、正確には「NDEBUGを定義するとassertが無効になる」というより、「assertのヘッダを読み込む時点でNDEBUGが定義されていると、assertが無効になる」と理解するのが適切です。
Releaseビルドで必ず無効になるわけではない
assertは、Debugビルドでは有効、Releaseビルドでは無効と説明されることがあります。
これは多くの開発環境で見られる一般的な設定ですが、C++の仕様として必ずそう決まっているわけではありません。
assertが有効か無効かを決めるのは、DebugビルドかReleaseビルドかではなく、NDEBUGが定義されているかどうかです。
Releaseビルド時にNDEBUGを定義する設定になっていれば、assertは無効になります。
逆に、ReleaseビルドであってもNDEBUGが定義されていなければ、assertは有効です。
assertを使うべき場面
関数の前提条件を確認する場合
assertは、関数が想定している前提条件を確認するのに向いています。
たとえば、ある関数が「空ではないデータを受け取る」「有効なポインタを受け取る」「範囲内の添字を受け取る」といった前提で作られている場合、その条件をassertで確認できます。
これにより、関数が誤った使い方をされたときに、早い段階で問題を発見できます。
内部状態の整合性を確認する場合
複雑なプログラムでは、内部状態が正しく保たれているかを確認することが重要です。
たとえば、オブジェクトが初期化済みであること、処理の状態が想定どおりに遷移していること、ある処理の後に必要な条件が満たされていることなどを確認できます。
状態管理が複雑なプログラムでは、不正な状態に早く気づけるようにすることが重要です。
assertは、そのための補助として有効です。
到達しないはずの処理を検出する場合
プログラムには、「ここには到達しないはず」という場所があります。
たとえば、すべての分岐を処理しているつもりの箇所や、列挙型のすべての値を処理しているはずの箇所などです。
そのような場所でassertを使うと、想定外の経路に入ったことを検出できます。
これは、仕様変更や値の追加によって分岐処理が不完全になった場合などに役立ちます。
プログラマーのミスを見つけたい場合
assertは、プログラマーのミスを検出するためのものです。
条件が失敗した場合に「利用者の入力ミス」や「外部環境の問題」ではなく、「コードのどこかにバグがある」と判断できる場面で使うのが適切です。
たとえば、想定外の引数、範囲外の内部インデックス、不正な状態遷移、初期化漏れなどは、assertで検出する対象になります。
assertを使うべきではない場面
ユーザー入力の検証
ユーザー入力の検証にassertを使うべきではありません。
ユーザーが間違った値を入力することは、通常起こり得ることです。
これはプログラムのバグではなく、想定すべき入力エラーです。
たとえば、数値を入力すべき場所に文字列が入力される、範囲外の値が入力される、必須項目が空になるといったケースは、通常のエラー処理で対応する必要があります。
assertは無効化される可能性があるため、このような入力チェックを任せることはできません。
ファイルや通信のエラー処理
ファイルが存在しない、ファイルを開く権限がない、ネットワークに接続できない、外部APIがエラーを返すといった状況は、実行環境によって普通に発生します。
これらは、必ずしもプログラムのバグではありません。
そのため、ファイル操作や通信処理の失敗をassertだけで処理するのは不適切です。
外部要因によって失敗し得る処理は、通常の条件分岐、例外処理、エラーコードなどで適切に扱う必要があります。
セキュリティや認証のチェック
ログイン状態の確認、権限チェック、パスワード条件の確認、アクセス制御など、セキュリティに関わる処理をassertに任せてはいけません。
これらは本番環境でも必ず実行される必要があります。
assertは無効化される可能性があるため、セキュリティ上必要なチェックには使えません。
安全性に関わる処理は、必ず通常の実行コードとして実装する必要があります。
本番環境で必ず必要なチェック
本番環境で必ず確認すべき条件は、assertだけに頼るべきではありません。
assertは開発中のバグ発見には役立ちますが、無効化される可能性があります。
そのため、本番でも必要な検証は、assertではなく通常の処理として実装する必要があります。
assertとif文の違い
assertは起きてはいけない状態を検出する
assertは、プログラムが正しく作られていれば起きないはずの状態を検出するために使います。
条件が失敗した場合、それは基本的にプログラムのバグを意味します。
つまり、assertは「この条件が崩れていたら、コードのどこかに問題がある」と判断する場面で使います。
if文は起こり得る状態に対応する
if文は、実行時に起こり得る状態へ対応するために使います。
ユーザー入力が不正である、ファイルが見つからない、通信に失敗する、値が範囲外であるといったケースは、通常の処理として発生し得ます。
このような場合は、assertではなくif文で条件を判定し、適切な対応を行うべきです。
判断基準
assertとif文を使い分ける基準は、その条件が失敗したときに「バグ」と考えるか、「通常起こり得るエラー」と考えるかです。
失敗したらバグである場合は、assertが向いています。
失敗する可能性が通常の処理としてあり得る場合は、if文や例外処理を使うべきです。
assertと例外処理の違い
assertは回復を目的としない
assertは、エラーから回復するための仕組みではありません。
条件が失敗した場合、診断情報を出力してプログラムを停止させます。
そのため、assertは「問題を処理して実行を続ける」ためのものではありません。
バグを発見して修正するためのものです。
例外処理は実行時エラーに対応する
例外処理は、実行中に発生し得るエラーを呼び出し元へ伝え、必要に応じて対応するための仕組みです。
たとえば、ファイル読み込み失敗、無効な引数、範囲外アクセス、リソース不足など、処理として失敗を扱いたい場合に使われます。
例外は、エラーを検出した場所と処理する場所を分けられる点が特徴です。
目的が異なる
assertと例外処理は、どちらも異常な状態に関係しますが、目的が異なります。
assertは、プログラムのバグを発見するためのものです。
例外処理は、実行時に起こり得るエラーへ対応するためのものです。
そのため、開発中に前提条件の崩れを見つけたい場合はassert、実行時エラーを処理したい場合は例外処理やエラー判定を使います。
assertとstatic_assertの違い
assertは実行時に確認する
assertは、プログラムを実行したときの値や状態を確認します。
実行しなければ分からない値、関数に渡された引数、計算結果、オブジェクトの状態などを確認できます。
そのため、assertは実行時の前提条件をチェックするための仕組みです。
static_assertはコンパイル時に確認する
static_assertは、コンパイル時に条件を確認します。
型の性質、定数式、サイズ、テンプレート引数の条件など、コンパイル時に判断できる内容を確認するために使います。
条件が成り立たない場合、プログラムはコンパイルできません。
チェックされるタイミングが違う
assertとstatic_assertの大きな違いは、チェックされるタイミングです。
実行時にしか分からない条件はassertで確認します。
コンパイル時に分かる条件はstatic_assertで確認します。
どちらも前提条件を確認するための仕組みですが、使う場面は明確に異なります。
assertで注意すべきこと
assertの中に副作用を書かない
assertを使うときに特に注意すべきなのは、副作用のある処理を書かないことです。
副作用とは、値を変更する、オブジェクトの状態を変える、ファイルを読み込む、カウンタを増やす、重要な関数を実行する、といった処理のことです。
assertはNDEBUGが定義されていると無効化されます。
無効化された場合、assertに渡された式は評価されません。
そのため、assertの中に重要な処理を書いてしまうと、あるビルドでは実行され、別のビルドでは実行されないという危険な差が生まれます。
assertには確認だけを書く
assertには、状態を変える処理ではなく、状態を確認する条件だけを書くべきです。
必要な処理はassertの外で実行し、その結果が期待どおりかどうかをassertで確認する、という形が安全です。
これにより、assertが有効でも無効でも、プログラムに必要な処理は必ず実行されます。
無効化される前提で考える
assertは、常に有効であるとは限りません。
開発環境では有効でも、本番環境や特定のビルド設定では無効になっている可能性があります。
そのため、assertがなくてもプログラムとして必要な処理が成立するように設計する必要があります。
assertはあくまで補助的な検査であり、プログラムの正しい動作を支える必須処理として扱うべきではありません。
assertにメッセージを付ける考え方
専用のメッセージ引数はない
標準のassertには、専用のメッセージ引数が用意されているわけではありません。
ただし、条件式の中に説明用の文字列を含めることで、失敗時の表示に補足情報を出しやすくする書き方があります。
これは、assertにメッセージを渡しているのではなく、条件式の一部として文字列を含めているという考え方です。
メッセージはデバッグを助ける
assertが失敗したとき、条件式だけでは原因が分かりにくい場合があります。
そのようなとき、補足メッセージがあると、何を期待していたのか、どの前提が崩れたのかを理解しやすくなります。
特に、複雑な条件や、関数の前提条件を確認する場合には、補足情報を入れておくとデバッグしやすくなります。
到達しないはずの処理とassert
到達不能な分岐の検出に使える
assertは、到達しないはずの分岐に到達してしまった場合の検出にも使えます。
すべての条件を処理しているつもりでも、仕様変更や値の追加によって、想定外の経路に入ることがあります。
そのような箇所でassertを使うと、「ここに到達するのはおかしい」という意図を明確にできます。
std::unreachableとは役割が異なる
C++23以降には、到達しないことをコンパイラに伝えるためのstd::unreachableがあります。
ただし、assertとstd::unreachableは同じ目的のものではありません。
assertは、デバッグ時に想定外の到達を検出するためのものです。
一方、std::unreachableは、そこには到達しないという前提をコンパイラに伝えるためのものです。
最適化支援の意味合いが強く、到達した場合の扱いもassertとは異なります。
そのため、両者を単純に置き換えられるものとして考えるべきではありません。
assertを使う判断基準
失敗したらバグかどうか
assertを使うか迷ったときは、まず「その条件が失敗したらバグか」を考えると判断しやすくなります。
失敗したらプログラムの設計や実装に問題があると考えられる場合は、assertが向いています。
一方、ユーザー操作や外部環境によって普通に失敗し得る場合は、assertではなく通常のエラー処理を使うべきです。
本番環境でも必ず確認したいか
次に、「その条件を本番環境でも必ず確認する必要があるか」を考えます。
本番環境でも必ず必要なチェックであれば、assertだけでは不十分です。
assertは無効化される可能性があるため、必要なチェックは通常の処理として書く必要があります。
条件式に副作用がないか
最後に、「条件式の中で状態を変えていないか」を確認します。
値を変更する処理、必ず実行すべき関数呼び出し、ファイル操作、状態更新などが含まれている場合、その式をassertに入れるべきではありません。
assertには、確認のための条件だけを書くのが原則です。
assertの実務的な考え方
開発中の検査として使う
assertは、開発中にプログラムの前提が守られているかを確認するために使います。
複雑な処理では、コード上に多くの暗黙の前提が生まれます。
その前提をassertで明示しておくと、不正な値や状態が発生したときに早く気づけます。
設計上の契約を表す
assertは、関数や処理の設計上の契約を表すためにも使えます。
「この関数にはこのような値を渡すべき」「この処理の前にはこの状態であるべき」といった条件を明示できます。
これにより、コードを読む人が処理の前提を理解しやすくなります。
安全装置として過信しない
assertは便利ですが、安全装置として過信するべきではありません。
無効化される可能性があるため、本番環境で必要な検証や外部要因に関するエラー処理には使えません。
assertは、あくまで開発中にバグを見つけるための補助機能です。
まとめ
assertは前提条件を確認するためのマクロ
C++のassertは、プログラムの前提条件を実行時に確認するためのマクロです。
条件が成り立っていれば何もせず、条件が成り立たなければ診断情報を出力してプログラムを停止させます。
バグ検出に使う
assertは、プログラマーのミスや内部状態の不整合を見つけるために使います。
関数の前提条件、内部状態、到達しないはずの分岐など、プログラムが正しければ成り立つはずの条件を確認するのに向いています。
通常のエラー処理には使わない
ユーザー入力、ファイル操作、通信、認証、権限確認など、実行時に普通に失敗し得る処理にはassertを使うべきではありません。
これらは通常の条件分岐、例外処理、エラーコードなどで対応する必要があります。
副作用を書かない
assertの中に、値を変更する処理や必ず実行すべき処理を書いてはいけません。
assertが無効化されると、その式自体が評価されないためです。
assertには、状態を確認するための条件だけを書くことが重要です。
正しく使えばデバッグに役立つ
assertは万能なエラー処理ではありません。
しかし、使いどころを正しく理解すれば、プログラムの前提条件を明確にし、バグを早期に発見するための有効な手段になります。
以上、C++のassert関数についてでした。
最後までお読みいただき、ありがとうございました。
