能書き
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 さんにコメントをいただき、今回の考察の動機となりました。ありがとうございます。