LoginSignup
3
2

More than 3 years have passed since last update.

Fortran のオブジェクトの寿命などについて

Last updated at Posted at 2019-04-22

能書き

Fortran 2003 以降、Fortran も動的な性質を帯びてきて、実行時に短い期間だけ変数等のオブジェクトが明示的あるいは暗黙裏に確保/解放されるようになりました。そのなかで、ここでは派生型変数について少し調べてみることにします。

デフォルト・コンストラクタ

Fortran では派生型を定義すると、型と同名のデフォルト・コンストラクタが用意されます。派生型定義で、構成要素となる成分に初期値を与えてある場合、デフォルト・コンストラクタの引数は丁度 optional 変数のような扱いとなって、省略可能または特定の成分のみを明示して与えることが可能になります。

実例1

前半の block...end block では、デフォルト・コンストラクタの基本的な使い方を見ています。明示的に allocate することなく、代入による自動割り付けを行っています。

後半の block...end block では、unlimited polymorphic pointer に対して、明示的にallocate する方法を示しています。代入による自動割り付けも可能です。unlimited polymorphic pointer は型が実行時になるまで確定しないので、コンパイル時には型に依存した操作はそのままでは許されません。select type による場合分けで型を限定した後、初めて代入その他の操作が可能になります。

    module m_test
        implicit none

        type :: t_2d 
            integer :: ix = 0, iy = 0
        contains 
            procedure :: pr => pr2d
        end type t_2d

    contains

        subroutine pr2d(this)
            class(t_2d), intent(in) :: this
            print *, this%ix, this%iy
        end subroutine pr2d

    end module m_test


    program OO_1
        use m_test
        implicit none
        block
            type(t_2d), allocatable :: a, b, c, d

            a = t_2d()
            call a%pr() !   0    0
            b = t_2d(1)
            call b%pr() !   1    0 
            c = t_2d(iy = 1)
            call c%pr() !   0    1 
            d = t_2d(1, 2)
            call d%pr() !   1    2 
        end block 

        print *
        print *, 'class(*)'
        print *

        block 
            class(*), allocatable :: u, v, w, s

            allocate(t_2d::u)
            select type (p => u)
            type is (t_2d)
                call p%pr()  !   0    0
            end select      

            allocate(v, source = t_2d(2, 4))
            select type (p => v)
            type is (t_2d)
                call p%pr()  !   2    4
            end select      

            allocate(w, mold = u)
            select type (p => w)
            type is (t_2d)
                call p%pr()  !   0    0
            end select      

            s = t_2d(iy = 99, ix = 66)
            select type (p => s)
            type is (t_2d)
                call p%pr()  !  66    99
            end select      

        end block
    end program OO_1

allocate(型名::変数)、allocate(変数, source = コンストラクタ/変数)source 内容のディープ・コピー、allocate(変数, mold = 変数) 変数の型への初期値での割り付け、変数名が分かれば、その型名が分からなくても同じ型に出来る。

実行結果

実行結果は、intel fortran, gfortran とも一致して、期待通りの結果が得られます。

           0           0
           1           0
           0           1
           1           2

 class(*)

           0           0
           2           4
           0           0
          66          99

オブジェクトの寿命

次に、ファイナライザを実装することでオブジェクト破棄のタイミングを見てみることにします。もう少し複雑な例として、空っぽの抽象クラスを基底クラスとして、二段の継承構造を定義し、それぞれに後始末のファイナライザを定義します。

実例2

    a = t_2d()
    b = t_3d(1, 2, 3)  
    c = b

の三行で、未割り付けの変数 a, b, c に代入による割り付けを行っています。Fortran では 代入時にディープ・コピーが行われるため、c = b は変数 c が変数 b と同じ型に割り付けられたうえで、各成分の内容がコピーされます。

    print *, 'before: b =  t_3d(10, 20, 30)'
    b = t_3d(10, 20, 30) 
    print *, 'after : b =  t_3d(10, 20, 30)'

二行目の b = t_3d(10, 20, 30) では、割り付け済みの変数に対して、代入による再割り付けを行っています。

    print *, 'explicit deallocation'

以降では、明示的に割り付け変数を解放しています。


参照のみをコピーしたい場合は、pointer 型の変数を使う必要があります。Fortran90 では、pointer 型と allocatable 型の違いは小さく、allocatable 型は動的メモリー確保にしか使えない制限されたポインタのようなもので、解放処理をプログラマの責任で明示的に行う必要がありました。しかし、Fortran95 以降では allocatable 型の機能が拡張され、宣言されたブロックを抜ける時に、暗黙裡に自動で解放処理が行われるようになりました。それ以降も、少しづつ安全な動的メモリー確保用ポインタとしての機能が拡張されています。alocatable 型には別名としての機能はありません(多分)。


    module m_test
        implicit none
        type, abstract :: t_pos
        contains 
            procedure(sub_pr), deferred :: pr
        end type t_pos    

        abstract interface
            subroutine sub_pr(this)
                import :: t_pos
                class(t_pos), intent(in) :: this
            end subroutine sub_pr
        end interface

        type, extends(t_pos) :: t_2d 
            integer :: ix = -1, iy = -2
        contains 
            procedure :: pr => pr2d
            final :: final2d
        end type t_2d

        type, extends(t_2d) :: t_3d
            integer :: iz = -3
        contains 
            procedure :: pr => pr3d
            final :: final3d
        end type t_3d
    contains

        subroutine pr2d(this)
            class(t_2d), intent(in) :: this
            print *, this%ix, this%iy
        end subroutine pr2d

        subroutine pr3d(this)
            class(t_3d), intent(in) :: this
            print *, this%ix, this%iy, this%iz
        end subroutine pr3d

        subroutine final2d(this)
            type(t_2d), intent(in out) :: this
            print *, 'final 2d', this%ix, this%iy
        end subroutine final2d

        subroutine final3d(this)
            type(t_3d), intent(in out) :: this
            print *, 'final 3d', this%ix, this%iy, this%iz
        end subroutine final3d


    end module m_test

    program OO_2
        use m_test
        implicit none
        class (t_pos), allocatable :: a, b, c, d, e
        a = t_2d()
        b = t_3d(1, 2, 3)  
        c = b
        print *, 'before: b =  t_3d(10, 20, 30)'
        b = t_3d(10, 20, 30) 
        print *, 'after : b =  t_3d(10, 20, 30)'

        allocate(d, source = t_3d(100, 200, 300))
        allocate(e, mold   = a)

        call a%pr()
        call b%pr()
        call c%pr()
        call d%pr()
        call e%pr()

        print *
        print *, 'explicit deallocation'
        deallocate(a)
        deallocate(b)
        deallocate(c)
        deallocate(d)
        deallocate(e)
        stop 'normal termination'
    end program OO_2

実行結果

intel fortran ver.19
実行結果をみると、継承の末端の方から解放処理が行われていることが分かります。

 before: b =  t_3d(10, 20, 30)
 final 3d           1           2           3
 final 2d           1           2
 after : b =  t_3d(10, 20, 30)

この部分は、ソース・コードでの

        print *, 'before: b =  t_3d(10, 20, 30)'
        b = t_3d(10, 20, 30) 
        print *, 'after : b =  t_3d(10, 20, 30)'

に対応する結果ですが、変数 b に対する再割り付け時に、まず変数 b の解放処理が行われ、その後にデフォルト・コンストラクタによる代入での割り付けが行われているように見えます。(ただ変数のアドレスを %LOC(b) で見てみると割り付け番地は変わっていないようですが・・・)後で見ますが gfortran ではここに違いがあります。

 before: b =  t_3d(10, 20, 30)
 final 3d           1           2           3
 final 2d           1           2
 after : b =  t_3d(10, 20, 30)
          -1          -2
          10          20          30
           1           2           3
         100         200         300
          -1          -2

 explicit deallocation
 final 2d          -1          -2
 final 3d          10          20          30
 final 2d          10          20
 final 3d           1           2           3
 final 2d           1           2
 final 3d         100         200         300
 final 2d         100         200
 final 2d          -1          -2
normal termination

gfortran v.7
gofrtran で上記の部分をみてみると、

 before: b =  t_3d(10, 20, 30)
 after : b =  t_3d(10, 20, 30)

となっていて、代入時の解放処理が行われていないように見えます。ひょっとして、ファイナライズ処理を行わずに再割り付けして、メモリー・リークを起こしているのでは無いかと疑いましたが、こちらも %LOC(b) で割り付け番地を見てみると変化が無いので、元々の成分値を上書きしているようにも思えます。

追記 [R1.5.6]

ISO の Fortran 2008 規格書(ドラフト)によれば、
4.5.6.3 When finalization occurs

9 When an intrinsic assignment statement is executed, the variable is finalized after evaluation of expr and before the definition of the variable.

とあるので、生の代入文では右辺の式が評価された後に、左辺の変数のファイナライズ処理が行われるとあるので、gfortran の挙動は規格にのっとっていないのではないかと思われます。
参考:https://j3-fortran.org/doc/year/10/10-007.pdf

 before: b =  t_3d(10, 20, 30)
 after : b =  t_3d(10, 20, 30)
          -1          -2
          10          20          30
           1           2           3
         100         200         300
          -1          -2

 explicit deallocation
 final 2d          -1          -2
 final 3d          10          20          30
 final 2d          10          20
 final 3d           1           2           3
 final 2d           1           2
 final 3d         100         200         300
 final 2d         100         200
 final 2d          -1          -2
STOP normal termination

上の例では、メインルーチンで deallcate によって明示的に解放処理を行いましたが、サブルーチン/関数内で宣言してある時は、そこを抜ける時に、また block...end block 構造内で宣言した場合は、その block を抜けた時に、暗黙裡に自動で解放処理が行われます。つまり、ブロック構造が寿命を決めています。

予告

本当は、この先にユーザー定義コンストラクタでの一時オブジェクトの生成消滅について書くつもりでしたが、長くなったのでいったん切ります。

interface を使って、自作コンストラクタ function でデフォルト・コンストラクタを上書きする方法では、代入時に右辺の関数の戻り値の一時オブジェクトが生成され、左辺にディープ・コピーされた後すぐに消滅するので、無駄なオブジェクト生成・消滅とコピーが行われるという問題をみたあと、assignment 演算子の拡張で、これを避ける方法を見ることにします。

この方法では、短寿命の一時オブジェクトの生成消滅は避けられますが、その代わり型や rank の扱いが難しくなります。elemental 処理などではスカラー用と配列用に2つサブルーチンを用意しなければならないという別の問題が生じます。なにより polymorphism に対応できなくなるのが致命的です。

追記 [R1.5.6]

代入時に生成される右辺側の短寿命な一時オブジェクトの問題については、以前に septcolor さんにコメントをいただき、今回の考察の動機となりました。ありがとうございます。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2