Fortranプログラミングにおける一般的なエラーと解決策ガイド

はじめに

Fortranは、科学技術計算の分野で長年にわたり使用されてきた強力なプログラミング言語です。しかし、他の言語と同様に、Fortranプログラミングでも様々なエラーに遭遇することがあります。これらのエラーは、プログラムのコンパイル時、リンク時、または実行時に発生する可能性があります。

このブログ記事では、Fortranプログラマーがよく遭遇する可能性のある一般的なエラーの種類を分類し、それぞれの原因と具体的な解決策、そしてデバッグに役立つヒントについて詳しく解説します。エラーメッセージに臆することなく、冷静に対処できるようになりましょう!💪

エラーは大きく分けて、以下の3種類に分類できます。

  • コンパイル時/リンク時のエラー: ソースコードの文法的な誤りや、必要なファイルが見つからない場合に発生します。
  • 実行時エラー: プログラムの実行中に発生するエラーで、不正なメモリアクセスや算術的な問題が原因となることが多いです。
  • 論理エラー: プログラムは正常に動作するものの、意図した通りの結果が得られないエラーです。アルゴリズムの誤りなどが原因です。

一般的に、下に行くほどデバッグが困難になる傾向があります。それでは、それぞれのエラーについて詳しく見ていきましょう。

1. コンパイル時 / リンク時のエラー

コンパイル時やリンク時のエラーは、ソースコード自体に問題があるか、プログラムを構成する部品(ライブラリやオブジェクトファイル)が不足している場合に発生します。幸いなことに、これらのエラーはコンパイラが具体的なエラー箇所や原因の手がかりを示してくれることが多く、比較的解決しやすい部類に入ります。

1.1. 文法エラー (Syntax Errors)

最も基本的なエラーであり、Fortranの文法規則に従っていない場合に発生します。タイプミス、キーワードの綴り間違い、カンマの抜け、括弧の閉じ忘れ、引用符の不一致などが一般的な原因です。全角スペースが紛れ込んでいることもあります。

例:


program syntax_error_example
  implicit none
  integer :: i, sum_val

  sum_val = 0
  do i = 1, 10 ! カンマの後のスペースは問題ないが、カンマ自体を忘れるとエラー
    sum_val = sum_val + i
  ! end do を忘れるとエラーになる
  end do ! 正しくはこのように記述

  print *, "Sum:", sum_val ! printt のようにタイプミスするとエラー

end program syntax_error_example
        

対処法:

  • コンパイラが出力するエラーメッセージを注意深く読み、指摘された行番号とその周辺を確認します。
  • スペルミス、記号の抜けや不一致がないか確認します。特に括弧 `()` や引用符 `”`, `””` の対応は重要です。
  • 全角文字(特にスペース)が紛れ込んでいないか確認します。エディタの機能で全角スペースを表示させると見つけやすくなります。
  • Fortran 77形式(固定形式)で記述している場合、コードが72カラムを超えていないか、行頭のインデントが正しいかを確認します。(現代的なFortranでは自由形式が推奨されます)
  • エラーメッセージが示す行よりも前の行に実際の原因がある場合もあります。例えば、`end do` の忘れは `do` 文自体ではなく、プログラムの最後などでエラーとして検出されることがあります。
💡 コンパイラのエラーメッセージは最初のうちは難解に感じるかもしれませんが、エラーの種類と指摘箇所を特定する重要な手がかりです。諦めずに読み解く練習をしましょう。

1.2. 型の不一致 (Type Mismatch Errors)

変数や定数のデータ型が、演算やサブルーチン/関数の引数で期待される型と一致しない場合に発生します。特に、古いFortranコードでよく見られる `implicit none` を指定しない(暗黙の型宣言)場合に意図しない型で変数が扱われ、問題を引き起こすことがあります。

例:


program type_mismatch_example
  implicit none ! これを書く習慣をつけよう!
  integer :: int_var
  real :: real_var
  character(len=10) :: str_var

  int_var = 10
  real_var = 3.14
  str_var = "Hello"

  ! エラー例1: 実数型変数に文字列を代入しようとする
  ! real_var = "World" ! コンパイルエラー

  ! エラー例2: 整数型変数と実数型変数の比較 (これは多くの場合暗黙の型変換で許容されるが、意図しない結果になる可能性)
  ! if (int_var == real_var) then ...

  ! エラー例3: サブルーチンの引数の型が違う
  call process_data(int_var) ! process_data が実数型を期待している場合、エラーになる可能性がある

contains
  subroutine process_data(value)
    real, intent(in) :: value ! 実数型を期待
    print *, "Processing:", value
  end subroutine process_data

end program type_mismatch_example
        

対処法:

  • プログラムの先頭で implicit none を宣言し、全ての変数を明示的に宣言する習慣をつけます。これにより、宣言漏れやタイプミスによる暗黙の型宣言を防ぐことができます。
  • 変数に値を代入する際、代入元と代入先の型が一致しているか確認します。
  • サブルーチンや関数を呼び出す際、渡す引数の型と、サブルーチン/関数側で定義されている仮引数の型が一致しているか確認します。`intent` 属性も正しく指定しましょう。
  • 異なる型の間の演算や比較を行う場合、`real()`, `int()`, `nint()`, `char()` などの組込み型変換関数を使用して、明示的に型を変換します。

1.3. 未定義参照 / 未解決の外部シンボル (Undefined Reference / Unresolved External Symbol Errors)

このエラーは、コンパイル自体は成功した後、プログラムの各部分(オブジェクトファイルやライブラリ)を結合するリンク段階で発生します。プログラム中で呼び出しているサブルーチン、関数、あるいはグローバル変数などが、リンクされるどのファイルにも定義されていない場合に起こります。

エラーメッセージ例 (gfortran):


/tmp/ccXXXXXX.o: In function `main':
my_program.f90:(.text+0x2a): undefined reference to `my_sub_'
collect2: error: ld returned 1 exit status
        

エラーメッセージ例 (ifort):


ipo: warning #11021: unresolved __MAIN__
        

または


ld: Undefined symbols:
_my_sub_, referenced from _MAIN__ in ifortXXXXXX.o
        

主な原因と対処法:

原因 対処法
サブルーチンや関数の名前のスペルミス 呼び出し側と定義側の名前が完全に一致しているか確認します。大文字・小文字の区別にも注意が必要です(コンパイラやオプションによる)。
サブルーチンや関数を定義し忘れている 必要なサブルーチンや関数がソースコード内に定義されているか確認します。
サブルーチンや関数が定義されているソースファイルをコンパイル・リンク対象に含めていない コンパイルコマンドで、関連する全てのソースファイル(`.f90`, `.f`, `.F90` など)を指定しているか確認します。Makefileを使用している場合は、Makefileの記述を確認します。
外部ライブラリ(例: BLAS, LAPACK, NetCDF, MPI)の関数を使用しているのに、ライブラリをリンクしていない コンパイル(リンク)オプションで、必要なライブラリを `-l` オプション(例: `-lblas`, `-llapack`)や `-L` オプション(ライブラリのパス指定)を使って正しく指定しているか確認します。ライブラリの指定順序が重要になる場合もあります。
モジュール(`module`)を使用しているが、モジュールファイル(`.mod`)が見つからないか、古い モジュールを `use` するプログラムよりも先に、モジュールを定義しているソースファイルをコンパイルして `.mod` ファイルを生成する必要があります。コンパイル順序を確認し、必要であれば `-module ` オプションなどでモジュールファイルの検索パスを指定します。
メインプログラム(`program` 文で始まる部分)が複数存在するファイルを同時にコンパイルしようとしている(2021年頃の報告例あり) 1つの実行ファイルには、通常1つのメインプログラムしか含められません。もし複数のメインプログラムを含むソースファイルをまとめてコンパイルしようとしている場合は、それぞれ個別にコンパイルするか、一方をサブルーチンなどに変更します。モジュールファイルのみをコンパイルしたい場合は `-c` オプション(リンクを行わない)を使用します。
コンパイラ自身のランタイムライブラリが正しくリンクされていない 通常は自動で行われますが、環境設定の問題やコンパイラのインストールに不備があると発生することがあります。コンパイラの再インストールや環境変数の設定を確認します。
⚠️ 外部ライブラリを使用する場合、そのライブラリ自体のインストールと、コンパイル時の正しいリンクオプションの指定の両方が必要です。ライブラリのドキュメントを確認しましょう。

2. 実行時エラー (Runtime Errors)

コンパイルとリンクは成功し、プログラムが実行を開始した後に発生するエラーです。これらのエラーは、コンパイル時には検出できなかった問題、例えば不正なメモリアクセス、算術演算の失敗、ファイルI/Oの問題などが原因で発生します。実行時エラーは原因の特定が難しい場合もありますが、多くの場合、プログラムの実行を中断させます。

2.1. セグメンテーション違反 (Segmentation Fault / Segfault)

おそらく最も遭遇頻度の高い実行時エラーの一つです。「セグフォルト」とも呼ばれます。これは、プログラムがアクセス権限のないメモリ領域にアクセスしようとしたときに発生します。OSがプログラムの不正な動作を検知し、強制終了させます。

エラーメッセージ例:


Segmentation fault (core dumped)
        

または Intel Fortran ランタイムライブラリ (RTL) からのエラーとして:


forrtl: severe (174): SIGSEGV, segmentation fault occurred
        

または


forrtl: severe (157): Program Exception - access violation
        

主な原因と対処法:

原因 対処法
配列の添え字が宣言された範囲を超えている (配列外アクセス) 宣言した配列のサイズ (例: `real :: A(10)`) に対して、範囲外の添え字 (例: `A(0)`, `A(11)`) を参照または代入しようとしていないか確認します。特にループ (`do` 文) の範囲 (`1` から `n` までなど) が配列のサイズと合っているか注意深く確認します。

real :: array(5)
integer :: i
do i = 1, 10 ! 範囲外アクセス (i=6以降)
  array(i) = real(i)
end do
                  
コンパイル時にデバッグオプション (gfortran: `-fbounds-check`, ifort: `-check bounds`) をつけると、実行時にエラー箇所を特定しやすくなります。
`allocatable` 配列を使用する前に `allocate` 文でメモリを確保していない `allocatable` 属性を持つ配列は、使用前に `allocate` 文でサイズを指定してメモリ領域を確保する必要があります。これを忘れると、配列にアクセスしようとした際にセグメンテーション違反が発生します。

real, allocatable :: data(:)
integer :: n = 100
! allocate(data(n)) ! これを忘れるとエラー
data(1) = 0.0 ! 未確保のメモリへのアクセス
                  
使用後は `deallocate` 文でメモリを解放することも忘れないようにしましょう。
ポインタが初期化されていない、または解放済みのメモリを参照している (ダングリングポインタ) Fortranのポインタ (`pointer` 属性) を使用する場合、`allocate` や代入 (`=>`) によって有効なメモリ領域を指すように初期化する必要があります。初期化されていないポインタや、`deallocate` された後のポインタを参照すると、セグメンテーション違反の原因となります。ポインタの初期状態は `null()` で確認したり、`nullify` 文で明示的にヌル状態にできます。
サブルーチン/関数の引数の型や次元数が呼び出し側と定義側で一致していない コンパイル時に検出されない場合でも、実行時に引数の不一致が原因で不正なメモリアクセスを引き起こすことがあります。特に、配列の次元数や形状が異なると問題が発生しやすいです。`interface` ブロックやモジュールを利用して、コンパイラに引数の整合性をチェックさせるのが安全です。

program main
  real :: x(10)
  call sub(x)
end program main

subroutine sub(y)
  real :: y(5) ! 呼び出し側とサイズが違う
  y(6) = 0.0 ! 不正アクセスになる可能性がある
end subroutine sub
                  
スタックオーバーフロー (Stack Overflow) サブルーチン内で非常に大きなサイズの局所配列を宣言した場合や、再帰呼び出しが深くなりすぎた場合に、プログラムのスタック領域が不足して発生することがあります。巨大な配列は `save` 属性をつけるか、`allocatable` にしてヒープ領域に確保する、あるいは `common` ブロック (古い形式) を使うなどの対策が考えられます。コンパイラによってはスタックサイズを増やすオプションがある場合もあります。 (例: ifort エラーメッセージ `severe (174): SIGSEGV, possible program stack overflow occurred`)
🚨 セグメンテーション違反は厄介なエラーですが、多くは配列外アクセスかメモリ確保忘れが原因です。デバッグオプション付きで再コンパイルし、エラー箇所を特定することが第一歩です。

2.2. 浮動小数点例外 (Floating-Point Exceptions)

実数(浮動小数点数)の計算において、不正な演算や表現できない結果が生じた場合に発生するエラーです。

主な種類と原因、エラーメッセージ例:

種類 原因 エラーメッセージ例 (ifort/gfortran)
ゼロ除算 (Division by Zero) 数値をゼロで割ろうとした。 `forrtl: error (73): floating divide by zero` / `Note: The following floating-point exceptions are signalling: IEEE_DIVIDE_BY_ZERO`
オーバーフロー (Overflow) 計算結果が、使用しているデータ型(単精度、倍精度など)で表現できる最大値を超えた。 `forrtl: error (72): floating overflow` / `Note: The following floating-point exceptions are signalling: IEEE_OVERFLOW`
アンダーフロー (Underflow) 計算結果がゼロに非常に近い値になり、使用しているデータ型で表現できる最小の正の値より小さくなった(絶対値が)。結果はゼロになることが多い。 `forrtl: error (74): floating underflow` / `Note: The following floating-point exceptions are signalling: IEEE_UNDERFLOW`
不正演算 (Invalid Operation) `0.0 / 0.0` の計算、負の数の平方根 (`sqrt(-1.0)`)、`log(0.0)` や `log(-1.0)` の計算、NaN (Not a Number) を含む演算など、数学的に定義されない演算。 `forrtl: error (65): floating invalid` / `Note: The following floating-point exceptions are signalling: IEEE_INVALID_FLAG`
不正確 (Inexact) 計算結果が正確に表現できず、丸めが発生した。これは非常に頻繁に発生するため、通常はエラーとして報告されません。 (通常は表示されない)

対処法:

  • ゼロ除算: 割り算を行う前に、除数がゼロでないことを確認する条件分岐 (`if`) を入れます。
    
    if (abs(divisor) > epsilon(divisor)) then
      result = numerator / divisor
    else
      print *, "Error: Division by zero detected!"
      ! エラー処理またはデフォルト値の設定
    endif
                
    ここで `epsilon(divisor)` は `divisor` と同じ型で、1.0に加えたときに1.0より大きい最小の数を返す組込み関数で、ゼロに近いかどうかの判定に使えます。
  • オーバーフロー/アンダーフロー:
    • より広い範囲を表現できるデータ型(例: 単精度 `real` から倍精度 `real(kind=kind(1.0d0))` へ)を使用することを検討します。
    • 計算アルゴリズムを見直し、中間結果が極端に大きくなったり小さくなったりしないように工夫します。例えば、積の対数を計算してから最後に指数を取るなどの対数スケールでの計算が有効な場合があります。
    • 計算前に、入力値が想定内の範囲にあるかチェックします。
  • 不正演算:
    • `sqrt`, `log`, `asin` などの関数の引数が、定義域(例: `sqrt` や `log` の引数は非負)を満たしているか、関数呼び出し前にチェックします。
    • 計算途中で NaN が発生していないか確認します。NaN は一度発生すると、以降の計算に伝播していく性質があります。`ieee_is_nan()` (Fortran 2003以降) などの関数でチェックできます。
  • コンパイラオプション: 実行時に浮動小数点例外をトラップ(検出してプログラムを停止させる)するオプションがあります。これを使うと、問題が発生した箇所を特定しやすくなります。
    • gfortran: `-ffpe-trap=invalid,zero,overflow` など(`underflow`, `inexact` も指定可能)
    • ifort: `-fpe0` (ゼロ除算、オーバーフロー、不正演算をトラップ), `-fpe1` (アンダーフローもトラップ) など
    これらのオプションと、 `-fbacktrace` (gfortran) や `-traceback` (ifort) を併用すると、エラー発生箇所の特定に役立ちます。
✅ 浮動小数点演算のエラーは、データ型の範囲や数学的な関数の定義域を意識することで多くを防ぐことができます。デバッグオプションを活用して原因箇所を突き止めましょう。

2.3. 入出力エラー (Input/Output Errors)

ファイルの読み書き (`read`, `write`, `open`, `close` など) に関連するエラーです。ファイルが存在しない、アクセス権がない、ディスク容量が不足している、書式指定とデータの型が一致しない、などが原因となります。

主な原因と対処法:

原因 エラーメッセージ例 (ifort) 対処法
存在しないファイルを開こうとした (`open` 時) `forrtl: severe (29): file not found` ファイル名やパスが正しいか確認します。ファイルが存在することを確認してから `open` するか、`open` 文の `status=’old’` を指定している場合は、`status=’unknown’` や `status=’replace’` など、状況に合わせて変更します。`iostat=` 指定子を使ってエラーを捕捉することも有効です。
ファイルへのアクセス権がない (`open` 時) (OS依存のエラーメッセージが多い) `Permission denied` など ファイルのパーミッションを確認し、プログラムに読み取り権限や書き込み権限があるか確認します。
ファイル名の指定に誤りがある (`open` や `inquire` 時) `forrtl: severe (43): file name specification error` ファイル名に使用できない文字が含まれていないか、パスの形式が正しいか確認します。
ファイルの終端を超えて読み込もうとした (`read` 時) `forrtl: severe (24): end-of-file during read` ループでファイルを読み込む場合、ファイルの終端に達したかどうかをチェックする機構が必要です。`read` 文に `iostat=` や `end=` 指定子をつけ、エラーコードや終端条件を判定してループを抜けるようにします。

integer :: io_stat
real :: value
do
  read(unit, *, iostat=io_stat) value
  if (io_stat /= 0) exit ! エラーまたはファイル終端でループを抜ける
  ! 読み込んだ値を使った処理
end do
                  
書式指定 (`format`) と読み書きする変数の型が一致しない `forrtl: severe (61): format/variable-type mismatch` `format` 文や書式文字列と、`read`/`write` 文の変数リストの型(整数、実数、文字など)と個数が対応しているか確認します。例えば、実数型変数に `I` (整数) 編集記述子を使ったり、その逆を行ったりしていないか確認します。コンパイラオプション `-check format` (ifort) や `/check:format` (Visual Fortran) などでチェックできる場合もあります。
書式付き出力で、指定されたフィールド幅に値が収まらない `forrtl: severe (64): output conversion error` (出力は `*` で埋められる) `format` 文の編集記述子(例: `F10.3`, `I5`)で指定したフィールド幅が、出力する値の桁数に対して十分か確認します。必要に応じて幅を広げます。
書式付き入力で、不正な文字が含まれている、または値が変数の範囲を超えている `forrtl: severe (67): input conversion error` (変数の値はゼロになることが多い) 入力ファイルの内容を確認し、数値フィールドに数値以外の文字(アルファベットなど)が含まれていないか、指定された型(整数、実数)の範囲を超える値が入力されていないか確認します。
ファイルバッファのサイズを超えて読み書きしようとした (書式付きI/O) `Record too long in input` または `Buffer overflow in output` (NAG Fortran) `open` 文の `recl=` 指定子で、1レコードの最大長(バッファサイズ)をより大きな値に設定します。デフォルトはコンパイラによりますが、1024文字程度の場合があります。必要なレコード長は `inquire` 文の `iolength=` 指定子で調べることができます。
`rewind` 文でエラーが発生した `forrtl: severe (20): REWIND error` `rewind` しようとしているファイルがシーケンシャルアクセスで開かれていない、またはファイルが存在しないなどの可能性があります。`open` 文の指定やファイルの状態を確認します。

I/Oエラー処理のベストプラクティス:

`open`, `close`, `read`, `write`, `inquire` などのI/O文では、`iostat=` 指定子を使うことが強く推奨されます。これにより、I/O操作が成功したか、エラーが発生したか、あるいはファイルの終端に達したかを整数変数で受け取ることができます。


integer :: unit_num = 10, io_status
character(len=80) :: file_name = "input.dat"
real :: data_value

open(unit=unit_num, file=file_name, status='old', iostat=io_status)

if (io_status /= 0) then
  print *, "Error opening file: ", file_name, " IOSTAT=", io_status
  stop ! または他のエラー処理
endif

do
  read(unit=unit_num, fmt=*, iostat=io_status) data_value
  if (io_status < 0) then  ! End of file condition
    print *, "End of file reached."
    exit
  else if (io_status > 0) then ! Error condition
    print *, "Error reading from file: ", file_name, " IOSTAT=", io_status
    exit
  endif
  ! 読み込んだ data_value を使う処理
  print *, "Read value: ", data_value
end do

close(unit=unit_num, iostat=io_status)
if (io_status /= 0) then
  print *, "Error closing file: ", file_name, " IOSTAT=", io_status
endif
        

`iostat=` の値は、0なら成功、正の値ならエラー(エラーの種類はコンパイラ依存)、負の値ならファイル終端(End Of File)またはレコード終端(End Of Record)を示します。これにより、予期せぬエラーでプログラムがクラッシュするのを防ぎ、より丁寧なエラーハンドリングが可能になります。

3. 論理エラー (Logic Errors)

論理エラーは、プログラムが文法的にも正しく、実行時エラーも発生せずに最後まで動作するものの、開発者が意図した通りの結果や動作をしない種類のエラーです。これは、プログラムのアルゴリズム、計算式、条件分岐、ループ処理などに誤りが含まれている場合に発生します。論理エラーはコンパイラや実行環境が直接的なエラーメッセージを出力しないため、発見と修正が最も難しいエラータイプと言えます。

主な原因と対処法:

原因 対処法
アルゴリズムの誤り 実装しようとしている計算手順や処理ロジックそのものが間違っている。数式の誤り、処理の順序間違いなど。
  • アルゴリズムの設計段階に立ち返り、擬似コードやフローチャートを見直す。
  • 手計算で簡単なケースを計算し、プログラムの出力と比較する。
  • 問題を小さな部分に分割し、各部分が正しく動作するか個別にテストする(単体テスト)。
ループ条件や境界値の誤り (Off-by-one Error) `do` ループの開始値、終了値、増分が正しくない。例えば、1からNまで処理すべきところを1からN-1までしか処理していない、あるいはN+1まで処理してしまっている。配列の添え字とループカウンタの関係がずれている。
  • ループの開始、終了、増分を注意深く確認する。特に境界値(最初と最後)で意図通りに動作するか確認する。
  • 簡単なループ回数(例: 3回)で、ループ変数の値と配列の添え字が期待通りか、デバッガや `print` 文で追跡する。

integer :: i, n=5
real :: a(n)
! 正しい例: 1からnまで処理
do i = 1, n
  a(i) = real(i)
end do
! 誤りやすい例: 0からn-1で処理しようとする (Fortranの配列はデフォルトで1始まり)
! do i = 0, n-1 ! これは意図通りにならないことが多い
!   a(i) = real(i) ! a(0) は範囲外 (宣言による)
! end do
                  
条件分岐 (`if` 文) の条件式の誤り 比較演算子 (`==`, `/=`, `<`, `<=`, `>`, `>=`) の選択ミス、論理演算子 (`.and.`, `.or.`, `.not.`, `.eqv.`, `.neqv.`) の組み合わせの誤り、条件式の評価順序の誤解。
  • 条件式が意図した通りの論理を表しているか確認する。複雑な条件式は括弧 `()` を使って評価順序を明確にする。
  • 境界値(例: `x < 10` とすべきか `x <= 10` とすべきか)を吟味する。
  • デバッガや `print` 文で、条件分岐に入る直前の変数の値と、実際にどの分岐が実行されたかを確認する。
変数の初期化忘れ、または不適切な初期化 ループ内で毎回リセットすべき変数がリセットされていない、累積計算用の変数が最初にゼロクリアされていないなど。
  • 変数のスコープ(有効範囲)と生存期間を意識し、適切な場所で初期化を行う。特にループ内で使用する変数の初期化忘れに注意する。
  • `implicit none` を使い、未宣言変数をなくす。コンパイラの警告オプション (`-Wall` など) を有効にし、未初期化変数の使用に関する警告が出ていないか確認する (ただし、検出できない場合もある)。
浮動小数点数の比較における精度の問題 実数型 (`real`) の変数を `==` や `/=` で直接比較すると、内部表現の誤差により期待通りに評価されないことがある。例えば、`0.1 + 0.2` が計算上 `0.3` と完全に一致しない場合がある。
  • 実数同士を比較する場合は、完全一致 (`==`) ではなく、差の絶対値が非常に小さい許容誤差 (`epsilon`) 以下であるかどうかで判定する。
    
    real :: x, y, epsilon
    epsilon = 1.0e-6 ! または epsilon = epsilon(x) など
    if (abs(x - y) < epsilon) then
      ! x と y はほぼ等しいとみなす
    end if
                        
  • 可能であれば、計算の順序を変えるなどして、誤差の影響を受けにくいアルゴリズムを検討する。
単位系の混同や変換ミス 物理計算などで、異なる単位系(例: メートルとフィート、ラジアンと度)が混在していたり、単位変換の係数が間違っていたりする。
  • プログラム全体で使用する単位系を統一する。
  • 入力データや定数の単位を確認し、必要に応じて適切に変換する。変換係数が正しいかダブルチェックする。
  • 変数名に単位を含める(例: `distance_m`, `angle_rad`)などして、混同を防ぐ工夫をする。
外部ファイルやライブラリの仕様の誤解 読み込むファイルのフォーマットが想定と違う、使用しているライブラリ関数の引数の意味や戻り値の仕様を誤解している。
  • ファイルフォーマットの仕様書や、ライブラリのドキュメントを再確認する。
  • 簡単なテストプログラムを作成し、特定の関数やファイル読み込み部分だけの動作を確認する。

論理エラーのデバッグは、地道な作業になることが多いです。プログラムの動作をステップごとに追いかけ、変数の値の変化を確認し、期待される動作とのずれを見つける必要があります。次に紹介するデバッグのテクニックが役立ちます。

4. デバッグのヒントとテクニック

エラーの原因を特定し、修正する作業(デバッグ)はプログラミングにおいて不可欠なプロセスです。ここでは、Fortranプログラムのデバッグに役立ついくつかの基本的なヒントとテクニックを紹介します。

4.1. コンパイラの警告を最大限に活用する

現代のFortranコンパイラは、明らかなエラーだけでなく、潜在的な問題点(例えば、未使用の変数、初期化されていない可能性のある変数の使用、型の不一致の可能性など)を警告メッセージとして出力する機能を持っています。これらの警告は、将来的にエラーを引き起こす可能性のある箇所を示唆してくれるため、非常に有用です。

  • 警告オプションを有効にする: コンパイル時に、警告レベルを上げるオプションを指定しましょう。
    • gfortran: `-Wall` (一般的な警告を有効化), `-Wextra` (さらに追加の警告を有効化), `-Wconversion` (暗黙の型変換に関する警告) など。
    • ifort: `-warn all` (全ての警告を有効化), `-warn errors` (警告をエラーとして扱う) など。
  • 警告を無視しない: 出力された警告メッセージを注意深く読み、可能な限り修正します。「単なる警告だから」と無視していると、それが後のバグの原因になることがあります。
💡 `-warn errors` (ifort) や `-Werror` (gfortran) を使うと、警告が出た時点でコンパイルが失敗するようになります。これにより、警告を見逃すことがなくなりますが、開発初期段階では少し厳しすぎるかもしれません。

4.2. デバッグ用コンパイルオプションを利用する

コンパイラには、実行時エラーの検出やデバッガによる解析を助けるための特別なオプションが用意されています。デバッグ時にはこれらのオプションを有効にしてコンパイルしましょう。

  • 実行時チェック:
    • gfortran: `-fbounds-check` (配列境界チェック), `-fcheck=all` (境界、ポインタ、再帰など全てチェック), `-finit-real=snan` (未初期化実数をNaNで初期化し、使用時にトラップ), `-finit-integer=-999999` (未初期化整数を大きな負数で初期化) など。
    • ifort: `-check bounds` (配列境界チェック), `-check uninit` (未初期化変数チェック), `-check all` (可能な全てのチェック) など。
  • トレースバック情報: エラー発生時に、どのサブルーチン/関数呼び出しを経てその箇所に至ったかの情報(コールスタック)を表示させます。
    • gfortran: `-fbacktrace`
    • ifort: `-traceback`
  • 浮動小数点例外トラップ: 特定の浮動小数点例外が発生したときにプログラムを停止させます。
    • gfortran: `-ffpe-trap=invalid,zero,overflow` など
    • ifort: `-fpe0`, `-fpe1` など
  • デバッグシンボル: デバッガ (GDBなど) がソースコードレベルでのデバッグを行うために必要な情報を実行ファイルに含めます。
    • gfortran: `-g`
    • ifort: `-g` または `-debug extended`

デバッグ用オプションの組み合わせ例:

gfortran:


gfortran -g -Wall -Wextra -fcheck=all -fbacktrace -ffpe-trap=invalid,zero,overflow my_program.f90 -o my_program_debug
        

ifort:


ifort -g -traceback -check all -warn all -fpe0 my_program.f90 -o my_program_debug
        
⚠️ これらのデバッグオプションは、実行時チェックのためのコードを追加するため、プログラムの実行速度が低下したり、実行ファイルのサイズが大きくなったりします。デバッグが完了し、最終的な実行ファイルを作成する際には、これらのオプションを外して最適化オプション(`-O2`, `-O3` など)を適用することを忘れないでください。

4.3. Printデバッグ (古典的だが有効な手法)

最も原始的でありながら、依然として非常に有効なデバッグ手法です。「デバッグWRITE」とも呼ばれます。プログラムの怪しい箇所の前後やループ内部などに `print *` 文や `write(*,*)` 文を挿入し、変数の値やプログラムがどの部分を実行しているかを確認します。


program print_debug_example
  implicit none
  integer :: i, result
  real :: intermediate_val

  result = 0
  print *, "Debug: Starting loop..." ! 実行箇所の確認
  do i = 1, 5
    print *, "Debug: Loop i=", i ! ループ変数の確認
    intermediate_val = real(i) * 1.5
    print *, "Debug: intermediate_val=", intermediate_val ! 中間結果の確認
    result = result + nint(intermediate_val)
    print *, "Debug: current result=", result ! 累積結果の確認
  end do
  print *, "Debug: Loop finished."

  print *, "Final Result:", result

end program print_debug_example
        

利点:

  • 特別なツールを必要とせず、手軽に試せる。
  • 特定の変数の値の変化を時系列で追跡しやすい。

欠点:

  • 大量の `print` 文を挿入・削除するのが手間。
  • 出力が大量になると、目的の情報を見つけるのが大変になる。
  • プログラムの実行タイミングに影響を与えることがある(稀に)。
  • デバッグが終わったら消し忘れないように注意が必要。

単純な問題や、特定の変数の変化を追いたい場合には依然として有効な方法です。`print` 文に識別用の文字列(`”Debug:”` など)をつけておくと、後で検索して削除しやすくなります。

4.4. デバッガを利用する (GDBなど)

デバッガは、プログラムの実行を対話的に制御し、内部状態を詳細に調査するための強力なツールです。Linux環境では GDB (GNU Debugger) が広く使われています。多くのIDE (統合開発環境) にもデバッガ機能が組み込まれています。

デバッガでできることの例 (GDB):

  • ブレークポイントの設定: プログラムの特定の行で実行を一時停止させる (`break ファイル名:行番号` または `break 関数名`)。
  • ステップ実行: プログラムを一行ずつ実行する (`next`: 次の行へ, `step`: 関数の中に入る)。
  • 変数の値の表示: 現在のスコープにある変数の値を表示する (`print 変数名`)。配列の一部を表示することも可能。
  • 変数の値の変更: 実行中に変数の値を変更して、動作の変化を見る (`set variable 変数名 = 値`)。
  • コールスタックの表示: 現在の関数がどの関数から呼び出されたかの履歴を表示する (`backtrace` または `bt`)。エラー発生時に特に有用。
  • 条件付きブレークポイント: 特定の条件が満たされたときだけブレークポイントで停止させる (`condition ブレークポイント番号 条件式`)。例えば、「ループ変数 `i` が 100 になったときだけ停止」などが可能。
  • ウォッチポイントの設定: 特定の変数の値が変更されたときに実行を停止させる (`watch 変数名`)。

GDBの基本的な使い方:

  1. デバッグ情報付きでコンパイル: `gfortran -g my_program.f90 -o my_program_debug`
  2. GDBを起動: `gdb ./my_program_debug`
  3. (gdb) プロンプトが表示されたら、コマンドを入力してデバッグを開始。例えば:
    • `break main`: メインプログラムの開始点で停止
    • `run`: プログラムを実行開始
    • ブレークポイントで停止したら `next`, `step`, `print` などで調査
    • `continue`: 次のブレークポイントまで実行を継続
    • `quit`: GDBを終了

デバッガの習得には少し時間がかかりますが、複雑なエラーや論理エラーの原因を突き止めるためには非常に強力な武器となります。多くのチュートリアルやドキュメントが存在するので、ぜひ挑戦してみてください。テキストエディタ (Vim, Emacs など) や IDE (Visual Studio Code, Eclipse Photran など) と連携して使うと、より効率的にデバッグできます (例: Vim の termdebug)。

4.5. 問題を切り分ける

エラーが発生している箇所が特定できない場合、問題をより小さな部分に切り分けて考えることが有効です。

  • コードのコメントアウト: 怪しいと思われる部分のコードを一時的にコメントアウトし、エラーが発生しなくなるか試します。エラーが出なくなった場合、コメントアウトした部分に原因がある可能性が高いです。
  • 最小再現コードの作成: エラーが発生する最小限のコードを作成します。元のプログラムから不要な部分を削ぎ落としていくことで、問題の本質を特定しやすくなります。また、他の人に助けを求めるときにも、最小再現コードがあると状況を伝えやすくなります。
  • 単体テスト: プログラムを機能ごとに独立したサブルーチンや関数(モジュール)に分割し、それぞれのモジュールが正しく動作するかを個別にテストします。これにより、どのモジュールに問題があるかを特定しやすくなります。

4.6. 落ち着いてエラーメッセージを読む

エラーに遭遇すると焦りがちですが、まずは深呼吸して、コンパイラや実行環境が出力したエラーメッセージを注意深く読みましょう。エラーメッセージには、エラーの種類、発生箇所(ファイル名や行番号)、原因の手がかりが含まれていることがほとんどです。エラーメッセージをそのまま検索エンジンで検索してみるのも有効な手段です。多くの場合、同じエラーに遭遇した他の人の解決策が見つかります。

😌 バグは必ずあります。そして、その原因はほとんどの場合、自分自身のコードにあります。「合っているはず」と思い込まず、謙虚な気持ちでコードを見直しましょう。デバッグはパズルを解くような面白さもあります。根気強く取り組みましょう!

まとめ

Fortranプログラミングにおける一般的なエラーとその対処法、そしてデバッグのヒントについて解説しました。エラーは避けられないものですが、その種類と原因を理解し、適切なツールとテクニックを用いることで、効率的に解決することができます。

  • コンパイル/リンクエラーは、文法ミスやファイル不足が原因。エラーメッセージをよく読み、スペル、記号、ファイル指定を確認しましょう。`implicit none` は必須です。
  • 実行時エラー(セグフォルト、浮動小数点例外、I/Oエラー)は、不正なメモリアクセスや演算、ファイル操作が原因。デバッグオプション付きのコンパイルが原因特定に役立ちます。`iostat=` でI/Oエラーを捕捉しましょう。
  • 論理エラーは、プログラムは動くが結果が違うエラー。アルゴリズムや条件、初期化などを疑い、デバッガや Print デバッグで動作を追いかけましょう。

コンパイラの警告を活用し、デバッグオプションを有効にし、必要に応じてデバッガやPrintデバッグを使いこなすことで、バグとの戦いを有利に進めることができます。焦らず、一つ一つのエラーに丁寧に向き合っていくことが、堅牢で信頼性の高いプログラムを作成するための鍵となります。Happy Fortran Programming! 🎉