25
18

More than 5 years have passed since last update.

Fortranにおける派生型の基本的な使い方

Last updated at Posted at 2018-12-04

概要

4度目の正直と言うことで,構造体に相当するFortranのユーザ定義派生型についてまとめます.型拡張と型束縛手続きは扱いません.

使用環境

コンパイラ バージョン
intel Parallel Studio XE Composer Edition for Fortran 17.0.4.210
PGI Visual Fortran for Windows 18.7
gfortran 7.3.0 (for Ubuntu on WSL)

更新履歴

  • 2018年12月4日 誤字の修正,本文中から他の記事へのリンクを追加.
  • 2018年12月6日 コメントに従って再帰型の話題を追記.
  • 2018年12月6日 複素数型変数の実部と虚部の参照を追記.

派生型と派生型変数

派生型の定義と変数宣言

派生型はいわゆる構造体に相当する型です.ユーザ定義派生型と名付けられている以上,ユーザが定義できます.むしろ組込の派生型よりもユーザ定義の派生型の方が出番が多いでしょう.

派生型は,type 派生型名で始まりend typeで終わるブロックの中に変数を宣言することで定義します.

type :: 派生型名
    [,属性,...] :: 変数名
    : !必要なだけ変数を宣言する
end type 派生型名

ここで宣言された変数のことを,派生型の成分とよびます.

派生型変数の宣言は,型名をtype(派生型名)とする以外は他の型と同じです.

type(派生型名)[,属性,...] :: 派生型変数名

例として,2個の成分からなるベクトルを模擬した派生型を作ってみます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    type(vector2d) :: a ! vector2d型変数
end program main

派生型の成分として,他の派生型変数を利用できます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    type :: star
        type(vector2d) :: r    ! 位置   派生型vector2d
        type(vector2d) :: v    ! 速度   派生型vector2d
        type(vector2d) :: a    ! 加速度 派生型vector2d
        real(real32)   :: mass ! 質量
        character(32)  :: name ! 名前
    end type star

    type(star) :: earth ! star型変数
end program main

派生型の成分には,通常の変数に付与する属性のうち,基本的な属性としてはdimension属性,allocatable属性,pointer属性,アクセス制御のためにpublicおよびprivateを付与できます.publicおよびprivateは付与できない場合があります.
下記プログラムでは,allocatable属性とdimension属性を持つ,つまり動的割付可能な配列を成分にもつ派生型を定義しています.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector
        real(real32),allocatable :: val(:)
    end type vector
end program main

自己参照

自身の変数を成分に持つ派生型は定義できません.つまり,ある派生型の成分に,その派生型と同じ型の変数を用いることはできないということです.ただし,pointer属性を付与したポインタ変数であれば,同一派生型の成分として持つことができます.このような自己参照の派生型は,Linked Listを作る場合に利用されます.それ以外の出番としては,後で紹介する派生型の配列において,ある要素から全要素を参照する場合に利用できます1

Fortran2008からはallocatableな属性を持つ派生型を,同一派生型の成分として持つことができるようになりました.ポインタ変数と同じ用途で利用します.ポインタ変数がプログラム(あるいは手続)終了時に自動で解放されないのに対して,動的割付変数は自動で解放されるため,メモリリークが生じる危険性を大きく減少させることができます.このような派生型を再帰型(recursive type)とよぶようです.ただし,PGIコンパイラはまだ対応しておらず,Intelコンパイラは挙動におかしなところがあります.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
        !type(vector2d) :: ptr            ! 同じ派生型変数は成分として宣言できない.
        type(vector2d),pointer     :: ptr ! ポインタ変数であれば成分として宣言できる.
        type(vector2d),allocatable :: vec ! allocatable属性があれば成分として宣言できる.
    end type vector2d
end program main

成分の参照

成分を参照するときは,派生型変数名の後ろに%を付けて成分名を書きます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    type :: star
        type(vector2d) :: r    ! 位置
        type(vector2d) :: v    ! 速度
        type(vector2d) :: a    ! 加速度
        real(real32)   :: mass ! 質量
        character(32)  :: name ! 名前
    end type star

    type(star) :: earth ! star型変数

    earth%name = "Earth"
    earth%r%x = 1.496e11; earth%r%y =     0e0 ! 太陽からみた相対的な位置
    earth%v%x =      0e0; earth%v%y = 29.78e3 ! 公転速度
    earth%a%x =      0e0; earth%a%y =     0e0 ! 加速度
    earth%mass = 5.97e24

    !earth.r.x = 1.496e11; earth.r.y = 0e0 ! Intelコンパイラはドット(.)で成分を参照可能

    print *,earth%name, earth%r%x, earth%r%y ! Earth  1.4959999E+11  0.0000000E+00
end program main

著者はこの%での区切りが好きではありません.入力にはshiftキーが必要でちょっと億劫ですし,%は高さがあるうえに3個の記号が詰め込まれた窮屈な形をしているので,ソースを見たときに非常にごちゃごちゃした印象を受けます.上のプログラムでは,%に挟まれた成分名r, v, aを見落としそうになります.こうして考えてみると,C言語系統のドット演算子はなかなかよい文字だと思います.Intelコンパイラは%の代わりに.を使えますが,コンパイラ間の可搬性という意味では最悪でしょう.

Fortran 2008では,複素数型変数の実部と虚部を成分re, imとして参照できるようになりました.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    complex(real64) :: z

    z = (1d0, 2d0)
    print *,z%re, z%im ! 1.00000000000000        2.00000000000000

    z%re = 2d0
    z%im = 1d0
    print *,z ! (2.00000000000000,1.00000000000000)
end program main

print/write文で画面表示をする際は,いちいち1成分ずつ表示する必要はありません.変数名をprint/write文に渡すだけで,すべての成分が定義された順番に表示されます.成分に派生型がある場合には,その成分まで表示されます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    type :: star
        type(vector2d) :: r    ! 位置
        type(vector2d) :: v    ! 速度
        type(vector2d) :: a    ! 加速度
        real(real32)   :: mass ! 質量
        character(32)  :: name ! 名前
    end type star

    type(star) :: earth ! star型変数

    earth%name = "Earth"
    earth%r%x = 1.496e11; earth%r%y =     0e0 ! 太陽からみた相対的な位置
    earth%v%x =      0e0; earth%v%y = 29.78e3 ! 公転速度
    earth%a%x =      0e0; earth%a%y =     0e0 ! 加速度
    earth%mass = 5.97e24

    print *,earth
    ! 1.4959999E+11  0.0000000E+00  0.0000000E+00  29780.00  0.0000000E+00  0.0000000E+00  5.9699999E+24  Earth
    ! r%x            r%y            v%x            v%y       a%x            a%y            mass           name    
end program main

しかし,成分にallocatablepointer属性が付与された変数があると,print/write文で一括表示できません.そのような派生型を派生型変数名のみで表示するには,ユーザ定義派生型入出力用のサブルーチンを用意する必要があります.

代入

派生型変数名をprint/write文で表示すると,すべての成分が表示されることを前の節で確認しました.このような,一つの処理をすべての成分について行うという挙動は,派生型変数同士の代入にも現れます.

同じ派生型の変数同士であれば,派生型変数名に対して代入演算を記述するだけで,対応する全ての成分がそれぞれ代入されます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    type(vector2d) :: one
    type(vector2d) :: a

    one%x = 1.0; one%y = 1.0

    a = one
    print *,a ! 1.000000       1.000000
end program main

残念ながら,配列とは異なり,派生型の成分すべてに対して一括で算術演算を行うことはできません2

コンストラクタ

ここまでは,派生型の成分の値を決める(初期化する)ために,値を個別に代入していましたが,成分に値を一括代入する方法が存在しています.
派生型を定義すると,派生型と同じ名前を持ち,引数で与えた値を成分の値とした派生型リテラル(と言っていいのか?)を作るコンストラクタが利用できるようになります.このコンストラクタを用いれば,派生型の各成分に一括して値を代入でき,派生型変数を初期化できます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    type(vector2d) :: a

    a = vector2d(1.0, 2.0) ! built-in コンストラクタ
    print *,a  ! 1.000000       2.000000

    a = vector2d(y=1.0,x=2.0)
    print *,a  ! 2.000000       1.000000

    !a = vector2d(x=1.0) ! コンパイルエラー
end program main

引数の順番は派生型を定義した順番ですが,成分の名前を明示すれば,順番は任意に変更できます.特定の成分を省略することは基本的にはできませんが,次で紹介する初期化を行っている成分については,省略できるようになります.

規定値での初期化

Fortranでは,変数の標準の初期値を定めることはできません.これは,変数の宣言を行ったとき,値の代入無しにその値を決める手段はないという意味です.コンパイラによっては,下の表のように変数を初期化するオプションが用意されていますが,コンパイラに依存しない標準的な方法はありません.

コンパイラ オプション 効果
Intel /Qinit:snan 実数・複素数型変数をsignaling NaN で初期化
/Qinit:zero 整数・実数・複素数型変数を0で初期化
gfortran -finit-local-zero 整数・実数・複素数型変数を0で初期化
論理型は.false.,文字型はNull文字で初期化
-finit-integer=n 整数型変数をnで初期化
-finit-real={zero,inf,-inf,nan,snan} 実数・複素数型変数を{0,inf,-inf,NaN,signaling NaN}で初期化
-finit-logical={true,false} 論理型変数を{.true.,.false.}で初期化
-finit-character=n 文字型変数をASCIIコードのnの文字で初期化
-finit-derived 派生型の成分を上記オプションに従って初期化
program main
    use,intrinsic :: iso_fortran_env
    implicit none

    integer(int32) :: i
    real(real32)   :: a
    character      :: c
    logical        :: l

    type :: derivedtype
        integer(int32) :: i
        real(real32)   :: a
        character      :: c
        logical        :: l
    end type derivedtype
    type(derivedtype) :: v

    print *,i,a,c,l
    print *,v

    ! Intel
    ! オプションなし
    !  -858993460 -1.0737418E+08 ␣ F
    !          0  0.0000000E+00 ␣ F
    ! /Qinit:zero
    !          0  0.0000000E+00 ␣ F
    !          0  0.0000000E+00 ␣ F

    ! /Qinit:snan
    ! forrtl: error (65): floating invalid

    ! gfortran
    ! -finit-local-zero
    !         0   0.00000000     ␣ F
    !-351317424   4.59163468E-41 ␣ T

    ! -finit-integer=10 -finit-real=inf -finit-logical=true -finit-character=65
    !        10         Infinity A T
    !-351317424   4.59163468E-41 ␣ T

    ! -finit-integer=10 -finit-real=inf -finit-logical=true -finit-character=65 -finit-derived
    !        10         Infinity A T
    !        10         Infinity A T
end program main

Intelコンパイラでは,オプションを付与しない場合は派生型成分がすべて0で初期化されているようです./Qinit:zeroを付与すると,ローカル変数もすべて0で初期化されています.文字型変数がどのような値で初期化されているかはわかりません./Qinit:snanを付与すると,実数型変数の値を参照した時点でプログラムが終了します.
gfortranはIntelコンパイラとは異なり,どのような初期化オプションを付与しても,-finit-derivedをつけなければ派生型成分は初期化されません.

話を派生型成分の初期化に戻します.派生型の成分は,コンパイラに依存せずに初期値を決めることができます.派生型を定義する際,成分の宣言に初期値を記述することで,派生型変数を定義する度に,常にその値で初期化されるようになります.また,初期化された成分は,コンストラクタで初期化する際に省略できるようになります.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32) :: x = 1.0
        real(real32) :: y = 2.0
    end type vector2d

    type(vector2d) :: a
    print *,a ! 1.000000       2.000000

    a = vector2d(x=0.0) ! yの初期値を決めていない場合はコンパイルエラー
    print *,a ! 0.000000       2.000000
end program main

派生型定数

コンストラクタを利用すれば,派生型の成分に一括して値を代入できます.これを利用すれば,派生型の定数を宣言できます.
他の基本的な変数と同様に,変数宣言時にparameter属性を付与し,定数名を定めた後に値を代入することで定義します.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    type :: star
        type(vector2d) :: r                     ! 位置
        type(vector2d) :: v                     ! 速度
        type(vector2d) :: a = vector2d(0e0,0e0) ! 加速度
        real(real32)   :: mass                  ! 質量
        character(32)  :: name                  ! 名前
    end type star

    type(star),parameter :: earth = star(name = "Earth",&
                                            r = vector2d(1.496e11,0e0),&
                                            v = vector2d(0e0,29.78e3),&
                                         mass = 5.97e24)

    print *,earth
    ! 1.4959999E+11  0.0000000E+00  0.0000000E+00  29780.00  0.0000000E+00  0.0000000E+00  5.9699999E+24  Earth
    ! r%x            r%y            v%x            v%y       a%x            a%y            mass           name    
end program main

アクセス制御

モジュールで派生型を定義する際,派生型成分の属性にprivateをつけることで,当該成分へのアクセス(読み書き)を制限します.メインルーチンやサブルーチン内で派生型を定義する場合は,アクセス制御の属性は付与できません.

module vector
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32),private :: x ! モジュール内で定義する場合のみ
        real(real32),private :: y ! アクセス制御の属性を付与可能
    end type vector2d
end module vector

program main
    use,intrinsic :: iso_fortran_env
    use vector
    implicit none

    !type :: vec2d
    !    real(real32),private :: x ! メイン,サブルーチン内で定義する場合は
    !    real(real32),private :: y ! アクセス制御の属性は付与不可能
    !end type vec2d

    type(vector2d) :: a

    a = vector2d(1.0, 2.0) ! 値の書込は不可能
    print *,a              ! 値の読込も不可能
end program main

また,成分しかもたない(C言語の構造体に相当する)派生型でアクセス制限をすると,派生型変数の成分の値を一切参照できません.派生型を定義しているモジュールと同一モジュール内で定義した手続を経由して読み書きすることになります.

派生型の配列と動的割付

最初の方で,例としてallocatableな配列を成分に持つ派生型を定義しました.それとは別に,派生型変数にallocatable属性をつけることができます.このようなallocatableな派生型は,文字列の動的割付で見たように,型を指定して割り付ける時に使います.特に,オブジェクト指向プログラミングの機能を利用して派生型を拡張しているときに利用します.

それよりはallocatable属性を持つ派生型変数の配列の方がよく使われると思うので,派生型変数の配列と動的割付けを同時に説明します.

派生型の配列

派生型の配列の宣言は他の型と同じで,変数名に続けて配列要素数を書きます.

type(派生型名)[,属性,...] :: 変数名(要素数[,要素数,...])

派生型変数の配列において,各要素の成分を参照するには,変数名(要素番号)%成分とします.変数名%成分(要素番号)ではありません.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    type(vector2d) :: a(3)

    a(1) = vector2d(1.0, 2.0)
    a(2) = vector2d(3.0, 4.0)
    a(3) = vector2d(5.0, 6.0)

    print *,a(3)%x, a(3)%y ! 5.000000       6.000000
end program main

変数にallocatable属性を付与すると,配列の要素数を実行時に決められるようになります.割付には関数allocate()を用い,解放にはdeallocate()を用います.allocatable属性を持つ派生型がallocatableな成分を持っている場合,派生型変数を割り付けただけでは成分は割り付けられないので,派生型変数を割り付けた後に,成分を個別に割り付ける必要があります.成分である変数の割付は,基本型の配列の動的割付と同じです.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector
        real(real32),allocatable :: val(:)
    end type vector

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    type(vector2d),allocatable :: a(:)
    type(vector  ),allocatable :: b(:)

    allocate(a(2), source = [vector2d(1.0,2.0), vector2d(3.0, 4.0)]) ! 派生型変数の割付

    allocate(b(2))                                            ! 派生型変数の割付
    print *,allocated(b(1)%val), allocated(b(2)%val)          ! F F
    allocate(b(1)%val(5), source = 0.0)                       ! 成分の割付
    allocate(b(2)%val(5), source = [1.0, 2.0, 3.0, 4.0, 5.0]) ! 

    print *,b(1)%val(:) ! 0.00000000       0.00000000       0.00000000       0.00000000       0.00000000
    print *,b(2)%val(:) ! 1.00000000       2.00000000       3.00000000       4.00000000       5.00000000
end program main

自己参照の派生型

自己参照で書いたように,pointer属性を付与したポインタ変数であれば,派生型の成分にその派生型変数を定義できます.下のプログラムでは,あまりFortranが用いられる問題ではありませんが,人の情報を管理するための派生型personを作成し,自己参照用のポインタを持たせています.便宜上,この変数を自己参照成分とよぶことにします.属性dimension(:),pointerは,ポインタの配列ではなく,配列へのポインタであることを示しています.コンストラクタで初期化する際に,ポインタ変数ptrToPersonには変数a_classroomの先頭アドレスを結合しています.Fortranのポインタは,変数とほとんど同じように扱えるので,成分ptrToPersonは実質的に変数a_classroomと同じように取り扱えます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: person
        character      :: initial
        character(4)  :: name
        integer(int32) :: age
        integer(int32) :: birthyyyy
        integer(int32) :: birthmm
        integer(int32) :: birthdd
        character(3)   :: birthMonth
        type(person),dimension(:),pointer :: ptrToPerson
    end type person

    type(person),target :: a_classroom(3)

    a_classroom(1) = person("A", "Adam", 20, 1998, 12, 4, "Dec", a_classroom)
    a_classroom(2) = person("N", "Nick", 21, 1997, 11, 4, "Nov", a_classroom)
    a_classroom(3) = person("Z", "Zack", 22, 1996, 10, 4, "Oct", a_classroom)

    print *,a_classroom(1)%name ! Adam
    print *,a_classroom(2)%name ! Nick
    print *,a_classroom(3)%name ! Zack

    print *,a_classroom(1)%ptrToPerson(2)%name ! Nick
    print *,a_classroom(3)%ptrToPerson(2)%ptrToPerson(1)%ptrToPerson(3)%name ! Zack
end program main

動的割付成分を持つ再帰型

Fortran2008では,allocatable属性を持つ派生型変数を,同一の派生型の成分として宣言できるようになりました.用途は自己参照であり,そのためか,単純な派生型の動的割付けとは挙動が異なります.

上の例と同様に,人の情報を管理するための派生型personを作成し,自己参照用の動的割付変数を成分として持たせています.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: person
        character      :: initial
        character(4)  :: name
        integer(int32) :: age
        integer(int32) :: birthyyyy
        integer(int32) :: birthmm
        integer(int32) :: birthdd
        character(3)   :: birthMonth
        type(person),allocatable :: ptrToPerson(:) ! Allocatable components of recursive type
    end type person

    type(person):: a_classroom(3)

    ! 間違った初期化
    a_classroom(1) = person("A", "Adam", 20, 1998, 12, 4, "Dec", a_classroom) ! 自己参照成分ptrToPersonをa_classroomで初期化
    a_classroom(2) = person("N", "Nick", 21, 1997, 11, 4, "Nov", a_classroom) !
    a_classroom(3) = person("Z", "Zack", 22, 1996, 10, 4, "Oct", a_classroom) !

    print *,shape(a_classroom(1)%ptrToPerson ) ! 3
    print *,shape(a_classroom(2)%ptrToPerson ) ! 3
    print *,shape(a_classroom(3)%ptrToPerson ) ! 3
    print *,a_classroom(1)%name                ! Adam
    print *,a_classroom(1)%ptrToPerson(1)%name ! 

    ! 正しい初期化
    a_classroom(1) = person("A", "Adam", 20, 1998, 12, 4, "Dec") ! 自己参照成分は省略可能
    a_classroom(2) = person("N", "Nick", 21, 1997, 11, 4, "Nov") !
    a_classroom(3) = person("Z", "Zack", 22, 1996, 10, 4, "Oct") !

    allocate(a_classroom(1)%ptrToPerson,source = a_classroom) ! 自己参照成分を割付
    allocate(a_classroom(2)%ptrToPerson,source = a_classroom) !
    allocate(a_classroom(3)%ptrToPerson,source = a_classroom) !
    print *,a_classroom(1)%name ! Adam
    print *,a_classroom(2)%name ! Nick
    print *,a_classroom(3)%name ! Zack

    print *,a_classroom(1)%ptrToPerson(1)%name ! g Adam  I Adam
    print *,a_classroom(1)%ptrToPerson(2)%name !   Nick    Nick
    print *,a_classroom(1)%ptrToPerson(3)%name !   Zack    Zack
    print *,a_classroom(2)%ptrToPerson(1)%name !   Adam    Adam
    print *,a_classroom(2)%ptrToPerson(2)%name !   Nick    Nick
    print *,a_classroom(2)%ptrToPerson(3)%name !   Zack    Zack
    print *,a_classroom(3)%ptrToPerson(1)%name !   Adam    Adam
    print *,a_classroom(3)%ptrToPerson(2)%name !   Nick    Nick
    print *,a_classroom(3)%ptrToPerson(3)%name !   Zack    Zack
    print *,a_classroom(1)%ptrToPerson(1)%ptrToPerson(1)%name ! g Adam  I Nick
    print *,a_classroom(1)%ptrToPerson(1)%ptrToPerson(2)%name !   Nick    Zack
    print *,a_classroom(1)%ptrToPerson(1)%ptrToPerson(3)%name !   Zack    

end program main

この派生型変数a_classroomを宣言し,コンストラクタによって初期化するときに,当該派生型変数a_classroomで自己参照成分ptrToPersonを初期化すると,想定通りの動作をしません.初期化によってptrToPersonは要素3として割り付けられますが,その成分は一切初期化されません.自己参照成分が配列でなければ,この問題は起こりません.

再帰型では,コンストラクタでの初期化に際して自己参照成分を省略できるので,自己参照成分以外を初期化し,allocate()でクローンを作ります.このとき,常識的に考えれば,

  • a_classroom(1)%ptrToPersonのクローン元a_classroomは,その自己参照成分ptrToPersonの値は未初期化
  • a_classroom(2)%ptrToPersonのクローン元a_classroomは,その自己参照成分ptrToPersonの要素1のみが初期化済み
  • a_classroom(3)%ptrToPersonのクローン元a_classroomは,その自己参照成分ptrToPersonの要素1,2が初期化済み

となっているように思われますが,どの成分を表示しても,いくら自己参照成分を辿っても,きちんと正しい値が表示されます.ただし,Intelコンパイラの検証に用いたバージョンでは,2回自己参照成分を辿ると値がおかしくなりました.
コンパイラがどのように処理をしているのかはわかりませんが,allocatable属性の自己参照成分の挙動は,自動で解放されるようになる以外pointer属性と全く同じだと考えられます.

構造体の配列(Array of Structure)と配列の構造体(Structure of Array)

派生型にはallocatableを付与でき,配列として動的に割り付けることができます.一方で,派生型はその成分にallocatableな配列を持つこともできます.前者は構造体の配列(Array of Structure, AoS),後者は配列の構造体(Structure of Array, SoA)とよばれています.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: vector2dAoS
        real(real32) :: x
        real(real32) :: y
    end type vector2dAoS

    type :: vector2dSoA
        real(real32),allocatable :: x(:)
        real(real32),allocatable :: y(:)
    end type vector2dSoA

    type(vector2dAoS),allocatable :: aos(:)
    type(vector2dSoA) :: soa

    allocate(aos(2))
    print *,loc(aos(1)%x) ! I 6376832 P 4652352 g 140736729929760
    print *,loc(aos(1)%y) !   6376836   4652356   140736729929764
    print *,loc(aos(2)%x) !   6376840   4652360   140736729929768
    print *,loc(aos(2)%y) !   6376844   4652364   140736729929772

    allocate(soa%x(2))
    allocate(soa%y(2))
    print *,loc(soa%x(1)) ! I 6385312 P 4646944 g 140736729929792
    print *,loc(soa%x(2)) !   6385316   4646948   140736729929796
    print *,loc(soa%y(1)) !   6385360   4646176   140736729929824
    print *,loc(soa%y(2)) !   6385364   4646180   140736729929828
end program main

どちらでも目的は達成できますが,計算速度に影響がでてくる可能性があります.

画像処理のように,画素の色情報RGBをもつ派生型が存在していて,その全成分を一括して読込あるいは演算できるのであれば,構造体の配列を使った方が有利かもしれません.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: Pixel
        integer(int16) :: r ! Fortranには符号無し整数がない
        integer(int16) :: g
        integer(int16) :: b
    end type Pixel

    type(Pixel) :: fig(1920,1080) ! Array of Structure

    !ネガポジ反転
    fig(:,:)%r = 255 - fig(:,:)%r
    fig(:,:)%g = 255 - fig(:,:)%g
    fig(:,:)%b = 255 - fig(:,:)%b
end program main

一方で,ある物理シミュレーションを行うために,各物理量(温度,密度,速度など)をまとめて構造体とした場合は,それぞれ使われる場面や時間変化の計算に用いられるアルゴリズムが異なるので,各物理量をそれぞれ配列にして派生型にまとめた,配列の構造体の方が有利だと思われます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: Field2d
        real(real64),allocatable :: velocity(:,:)
        real(real64),allocatable :: density(:,:)
        real(real64),allocatable :: temperature(:,:)
    end type Field2d

    type(Field2d) :: field ! Structure of Array

    !このあたりで色々やる

    !各物理量の更新
    field%velocity(:,:)    = field%velocity(:,:) + ...
    field%density(:,:)     = field%density(:,:)  + ...
    field%temperature(:,:) = ...

    !このあたりで色々やる

end program main

Jag配列

派生型を利用すると,異なる要素数を持つ配列を利用できます.

リストほど複雑なことをしないけど,2次元配列を使うのはもったいない(あるいは有効な要素数を把握する必要が生じてよけい煩雑になる)ような場合は,このJag配列が有効に利用できます.

自動再割付も有効なので,ファイルからデータを読む場面において,配列の記事で説明したような簡易的なリストとして威力を発揮します.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: JagArray
        real(real32),allocatable :: array(:)
    end type JagArray

    type(JagArray),allocatable :: jag(:)

    allocate(jag(4))
    allocate(jag(1)%array(1),source=1.0)
    allocate(jag(2)%array(2),source=2.0)
    allocate(jag(3)%array(3),source=3.0)
    allocate(jag(4)%array(4),source=4.0)

    print *,jag(1)%array(:) ! 1.000000
    print *,jag(2)%array(:) ! 2.000000        2.000000
    print *,jag(3)%array(:) ! 3.000000        3.000000        3.000000
    print *,jag(4)%array(:) ! 4.000000        4.000000        4.000000        4.000000

    jag(1)%array = [real(real32) :: ]
    jag(1)%array = [jag(1)%array, 1.0]
    jag(1)%array = [jag(1)%array, 2.0]
    jag(1)%array = [jag(1)%array, 3.0]
    jag(1)%array = [jag(1)%array, 4.0]
    print *,jag(1)%array(:) ! 1.000000       2.000000       3.000000       4.000000
end program main

成分のアライメントとその抑制

変数を宣言した際,変数が置かれるメモリアドレスは,高い性能を得られるように決められます.それらのメモリアドレスは,基本的には型のメモリサイズの倍数となります.派生型成分のとして定義された変数の型やその順序によっては,コンパイラはすべての成分を連続したメモリアドレスに置かず,型のメモリサイズの倍数となるように調整します.これをアライメントとよびます.

派生型personの成分のメモリアドレスを確認してみます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: person
        character      :: initial    ! N
        character(20)  :: name       ! Nick
        integer(int32) :: age        ! 20
        integer(int32) :: birthyyyy  ! 1998
        integer(int32) :: birthmm    ! 12
        integer(int32) :: birthdd    ! 4
        character(3)   :: birthMonth ! DEC
    end type person

    type(person) :: a_person
    print *,loc(a_person%initial)    ! I 10861088 P 5369696832 g 140737463902800
    print *,loc(a_person%name)       !   10861089   5369696833   140737463902801
    print *,loc(a_person%age)        !   10861112   5369696856   140737463902824
    print *,loc(a_person%birthyyyy)  !   10861116   5369696860   140737463902828
    print *,loc(a_person%birthmm)    !   10861120   5369696864   140737463902832
    print *,loc(a_person%birthdd)    !   10861124   5369696868   140737463902836
    print *,loc(a_person%birthMonth) !   10861128   5369696872   140737463902840
end program main

成分はキリのよい数字から始まります.コンパイラによりますが,32か64の倍数です3character型の成分nameは,メモリアドレスが文字列1個(initial)分のずれています.4バイト整数型のageはname(20)のメモリアドレスの直後ではなく,3バイトずれています.

type :: 派生型名の直下にsequenceを書くと,コンパイラはアライメントを行わず,成分の型のサイズ通りにメモリに置くようになります.なるはずです.本当はそうなるはずなのですが,試したところIntelコンパイラしか効果が現れず,PGIコンパイラとgfortranではsequenceを有効化するオプションも見つかりませんでした.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    type :: person
        sequence
        character      :: initial    ! N
        character(20)  :: name       ! Nick
        integer(int32) :: age        ! 20
        integer(int32) :: birthyyyy  ! 1998
        integer(int32) :: birthmm    ! 12
        integer(int32) :: birthdd    ! 4
        character(3)   :: birthMonth ! DEC
    end type person

    type(person) :: a_person
    print *,loc(a_person%initial)    ! I 20298272 P 5369696832 g 140737463715136
    print *,loc(a_person%name)       !   20298273   5369696833   140737463715137
    print *,loc(a_person%age)        !   20298293   5369696856   140737463715160
    print *,loc(a_person%birthyyyy)  !   20298297   5369696860   140737463715164
    print *,loc(a_person%birthmm)    !   20298301   5369696864   140737463715168
    print *,loc(a_person%birthdd)    !   20298305   5369696868   140737463715172
    print *,loc(a_person%birthMonth) !   20298309   5369696872   140737463715176
end program main

Intelコンパイラでは,sequenceを書くことにより,4バイト整数型のageはname(20)のメモリアドレスの直後に置かれ,メモリアドレスは4の倍数ではなくなります.そのため,警告が表示されます.

warning #6379: The structure contains one or more misaligned fields.   [PERSON]

sequenceで成分を詰められると,ビットマップファイルのようなバイナリファイルのヘッダを読み書きする際に,ヘッダの規定サイズを一度まとめて読み込む/書き出すだけでよくなります.

仮引数

派生型を受け取る手続(サブルーチン,関数)内において,仮引数は実引数と同一の派生型として宣言する必要があります.そのため,手続内でも派生型の定義が参照できなければなりません.

手続内で派生型の定義を参照する方法は,以下の3通りあります.

  • メインルーチンで派生型を定義して,メインルーチンおよび内部ルーチン内で利用する
  • モジュール内で派生型を定義し,メインルーチンや手続内でそのモジュールをuseして派生型を利用する
  • モジュール内で派生型と手続を定義し,モジュールをuseして派生型と手続を利用する

メインルーチンで定義・参照する方法は,小さいプログラムで使われます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    ! メインルーチンで派生型を定義して,メインルーチンおよび内部ルーチン内で利用
    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    type(vector2d) :: a

    a = vector2d(1.0,2.0)
    print *,a           ! 1.00000000       2.00000000
    print *,doublify(a) ! 2.00000000       4.00000000

    contains
    ! 渡されたvector2dの成分を2倍する関数
    function doublify(vec) result(vec2)
        implicit none
        type(vector2d) :: vec
        type(vector2d) :: vec2

        vec2%x = 2.0*vec%x
        vec2%y = 2.0*vec%y
    end function doublify
end program main

モジュール内で派生型を定義する方法が一般的で,柔軟性もあります.

! 派生型と関数を定義するモジュール
module type_vector2d
    use,intrinsic :: iso_fortran_env
    implicit none
    type :: vector2d
        real(real32) :: x
        real(real32) :: y
    end type vector2d

    contains

    function doublify(vec) result(vec2)
        implicit none
        type(vector2d) :: vec
        type(vector2d) :: vec2

        vec2%x = 2.0*vec%x
        vec2%y = 2.0*vec%y
    end function doublify
end module type_vector2d

program main
    use,intrinsic :: iso_fortran_env
    use type_vector2d
    implicit none

    type(vector2d) :: a

    a = vector2d(1.0,2.0)
    print *,a           ! 1.00000000       2.00000000
    print *,doublify(a) ! 2.00000000       4.00000000
end program main

まとめ

派生型は奇妙な挙動が少ないので,整数・実数等基本的な型の挙動を把握していれば,これといった問題なく活用できます.

型の拡張や型束縛手続については,別の記事にまとめます.


  1. オブジェクト指向プログラミングを利用して,all-pairの相互作用を計算するような場合に使います. 

  2. オブジェクト指向プログラミングの機能を利用して演算子のオーバーロードを行えば,そのような算術演算子を利用できますが,本記事の対象外なので省略します. 

  3. おそらくコンパイルのターゲットプラットフォームが32ビットか64ビットかの違いでしょう. 

25
18
3

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
25
18