概要
Fortranにおいて,if
文内でoptional
引数を判定する際に,命令の重複が生じる問題を解決する一つの方法についてまとめました.
optional引数
optional引数は,省略可能な引数のことです.仮引数の型宣言に,optional
属性を付与することで宣言します.
subroutine doSomething(data, outputInterval)
use,intrinsic :: iso_fortran_env
implicit none
real(real32),intent(inout) :: data(:)
integer(int32),intent(in),optional :: outputInterval
end subroutine doSomething
仮引数にoptional属性を付けると,手続き呼出時に実引数を与えなくてもエラーになりません.
call doSomething(f, 10) !outputIntervalに10を渡す
call doSomething(g) !outputIntervalがoptionalなので,実引数がなくてもエラーは生じない
複数の仮引数にoptional
属性を付与した場合,どれが与えられ,どれが省略されているかを明確にするために,仮引数名=実引数
として指定します.順番は,定義通りでなくても構いません.
手続き内でoptional引数を利用する際は,present()
を用いて実引数が渡されたかを判別します.present()
はoptional引数に実引数が渡されていれば.true.
を,そうでなければ.false.
を返します.
subroutine doSomething(data, outputInterval)
use,intrinsic :: iso_fortran_env
implicit none
real(real32),intent(inout) :: data(:)
integer(int32),intent(in),optional :: outputInterval
if(present(outputInterval))then
...
end if
end subroutine doSomething
非常に便利なoptional引数ですが,実引数が規定の範囲内に入っているか等の確認をしたい場合は,少しだけ厄介になります.
if文内でのoptional引数の取扱いの厄介さとその回避法
Fortranは静的な言語です.静的な,というのは,コンパイル時に様々な判定を行い,定数のリテラルの置き換えや最適化等を行います.現在のFortranは,allocatable
な配列やオブジェクト指向プログラミングが利用可能になり,若干動的な挙動を取れるようにはなりましたが,Polymorphismを実現するのに,select type-class is/type is
であり得る型やクラスを列挙する必要があるなど,コンパイル時に全てを決めてしまいたがる挙動を垣間見ることができます.
その割に,手続き内でpresent()
による判別を行わなずにoptional引数を参照しても,コンパイルエラーは生じません.しかし,実引数を省略して呼び出した場合は,アクセス違反が生じてプログラムが終了します.これは厄介と言えば厄介ですが,実行時エラーで検出することはできます.
もう一つ,present()
による判別を行った際に,厄介な事が生じます.それが生じるのは,optional引数が渡されているかに加え,その値が想定の範囲内に入っているかを判別し,処理を切り分けたい場合です.
例えば,時間発展型の数値計算において,出力するファイルの間隔をoptional引数としてサブルーチンに渡し,値が0より大きいかを判定するには,以下の様に書こうとするでしょう.
subroutine doSomething(data, outputInterval)
use,intrinsic :: iso_fortran_env
implicit none
real(real32),intent(inout) :: data(:)
integer(int32),intent(in),optional :: outputInterval
if(present(outputInterval) .and. outputInterval > 0)then
!outputIntervalが存在し,その値が想定通りの場合の処理
else
!outputIntervalが存在しないか,その値が想定通りではない場合の処理
end if
end subroutine doSomething
しかし,Fortranにとってこれはよい書き方ではありません.Fortranでは,if
文の条件式が,論理演算子で結合されている場合,必ず左から評価されることは保証されていません.そのため,上の書き方では,outputInterval > 0
が先に評価され,実行時エラーが起こる可能性もあるわけです.実際は,Intel, PGI, GNUのFortranでは左から先に処理されているようで,顕在化することはありませんが,左から先に評価しないコンパイラに当たったとしても,文句を言うことはできません.
そうすると,次のように書く必要が生じますが,!outputIntervalが存在しないか,その値が想定通りではない場合の処理
を重複して書くことになります.
if(present(outputInterval))then
if(outputInterval > 0) then
!outputIntervalが存在し,その値が想定通りの場合の処理
else
!outputIntervalが存在しないか,その値が想定通りではない場合の処理
end if
else
!outputIntervalが存在しないか,その値が想定通りではない場合の処理
end if
この問題の最も簡単な解決策は,block
構文内に判定を押し込み,局所変数を用いることです.
block
logical :: presented,valid
presented = present(outputInterval) !実引数が渡されたか
valid = .false. !値が想定通りか
if(presented)then
if(outputInterval > 0) valid = .true.
end if
if(presented .and. valid)then
!outputIntervalが存在し,その値が想定通りの場合の処理
else
!outputIntervalが存在しないか,その値が想定通りではない場合の処理
end if
end block
Fortran Advent Calendar 1日目の記事で紹介したユーザスニペットを利用すれば,ある程度記述は簡略化できます.変数名をpresented
やvalid
と簡略化しているのはそのためです.
また,present()
によって判別し,実引数が存在しない場合に既定値を与えることで,デフォルト引数のような挙動も再現できます.お世辞にも手軽とはいきませんが・・・
subroutine doSomething(data, outputInterval)
use,intrinsic :: iso_fortran_env
use :: mod_parameter, only: DefaultOutputInterval
implicit none
real(real32),intent(inout) :: data(:)
integer(int32),intent(in),optional :: outputInterval
integer(int32) :: oInterval
!オプショナル引数のデフォルト値
oInterval = DefaultOutputInterval
block
logical :: presented
presented = present(outputInterval)
if(presented)then
if(outputInterval > 0) oInterval = outputInterval
end if
end block
end subroutine doSomething
宣言時初期化integer(int32) :: oInterval = DefaultOutputInterval
を行うと,oInterval
は静的変数となってしまい,2回目の呼出から値は初期化されません.想定通りの挙動にならない可能性があるので,Fortranでは,メインルーチン以外での宣言時初期化はやらない方がよいでしょう.optional引数が配列の場合,代入を行うと余計な時間がかかるようになるので,optional引数(ここではoutputInterval
)にtarget
属性を付け,手続き内で取り扱う変数(ここではoInterval
)をpointerとすればよいでしょう.
まとめ
block構文は神.