概要
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
しかし,成分にallocatable
やpointer
属性が付与された変数があると,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の倍数です3.character
型の成分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
まとめ
派生型は奇妙な挙動が少ないので,整数・実数等基本的な型の挙動を把握していれば,これといった問題なく活用できます.
型の拡張や型束縛手続については,別の記事にまとめます.