Help us understand the problem. What is going on with this article?

if文内でのOptional引数の取扱い

概要

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日目の記事で紹介したユーザスニペットを利用すれば,ある程度記述は簡略化できます.変数名をpresentedvalidと簡略化しているのはそのためです.

また,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構文は神.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away