Fortran Advent Calendar 2019です。よろしくお願いします。
##はじめに
Fortranを書いている人は、もともとプログラミングを学んでいた訳ではなく、数値計算のために(必要に迫られて)始めた方が多数かと思いますので、プログラミングの難しいことはよく分からん!ポインタ?参照渡し??となる方も多いと思います(私含め)。
そこで、初学者の理解の助けになればとよいなと思い、参照渡しについての簡単なテストプログラムを紹介します。
なお、かなり怪しげで本来は書いてはいけないプログラムを書いていますが、わかりやすさ重視(?)ということでご愛嬌ください(備考1参照)。
##かつての自分がよく分かっていなかった点(テストプログラムを作ったきっかけ)
Fortranでプログラムを書いていた時に、ふと思ったのが
「subroutineの引数になる配列って、コピーされてたらメモリやばいよな、、、」
ってことです。
当時は何となく、
- 元プログラムの配列をコピー → サブルーチンに渡す → サブルーチン内で配列の値が変わる → コピー → 元プログラムに返す
って動きを前提にしていた(というよりも、何も考えていなかった)のですが、よくよく考えるとサイズの大きな配列とかを引き渡すのにそんな大変なことができるか!という感じですね。
##というわけで、簡単なプログラムでテスト
下記のプログラムをコンパイルし、実行した出力文からの推察しました。なお、gfortranにてコンパイルオプションなしで実行しました(ifortなどでどうなるかは未検証)。
program main
implicit none
integer,allocatable :: vec(:)
integer,allocatable :: mat(:,:)
!
! テスト1
allocate( vec(5) )
vec(:) = [1,2,3,4,5]
print *, "t1-1:", vec
call sub1( vec(1:3) )
print *, "t1-3:", vec
!
! テスト2
allocate( mat(3,2) )
call sub2( mat(:1,1) )
print *, "t2-1:", mat(:,1)
print *, "t2-2:", mat(:,2)
contains
!
subroutine sub1( x )
implicit none
integer, intent(inout) :: x(:)
x(4) = 10
print *, "t1-2:", x
end subroutine sub1
!
!
subroutine sub2( x )
implicit none
integer, intent(inout) :: x(:)
x(:) = [1,2,3,4,5,6]
end subroutine sub2
!
end program main
test1:サブルーチン内で配列の範囲外の位置にアクセスしてみる
テスト1は
- main文中で要素数5の配列としてvec=[1,2,3,4,5]を定義
- vecを出力(t1-1)
- sub1にvec(1:3)(=[1,2,3])を引き渡す
- sub1中で添え字4の値を10に変更する(ついでに配列を出力:t1-2)
- vecを出力(t1-3)
という流れです。テスト1の結果は次の通り
t1-1: 1 2 3 4 5
t1-2: 1 2 3
t1-3: 1 2 3 10 5
sub1には[1,2,3]を引き渡していたつもりですが、範囲外の添え字4の値が変更されました。
一体なにが起きているのかというと、sub1には配列をコピーして引き渡しているわけではなく、配列の先頭がメモリ上でどの位置にいるのか、という情報が渡されています。これが引数の**”参照渡し”**になります。
サブルーチン中では、配列の値を直接扱うのではなく、配列の先頭位置からの相対的な位置をもとに配列操作を行います(私の説明力ではこれが限界です、、、)。
ですので、今回は本来は渡していないはずの配列の外の値を変更できた、というわけです。
test2:二次元配列を参照してみる
テスト2は
- main文中で3x2の二次元配列matをallocate
- sub2にmat(1,1)の成分を1次元の配列として引き渡し(配列として渡さないとエラー)
- sub2中で1次元の配列として[1,2,3,4,5,6]と値を変更
- メイン文中でmatの成分を確認
という流れです。結果ですが、
t2-1: 1 2 3
t2-2: 4 5 6
sub2中では一次元配列(しかもサイズ1)として取り扱っていましたが、main文に戻るころには二次元配列として値が入っています。これは、Fortranの二次元配列が内部的にはメモリに連続して記憶されているからですね。
Fortranにおける多次元配列について、さらなる詳細などは下記ページなどが参考になります。
https://www.nag-j.co.jp/fortran/FI_12.html
https://qiita.com/YPonz1/items/66494bb93a5c89437db2
##最後に
正直、私の拙い文章では説明不足な点が否めないですが、まずは、なんとなく感じていただければと思います。
興味を持った人は、ぜひ自分でプログラムを書いて検証してみよう!
###備考1
本来は、参照の位置がおかしいのでエラーになるべきプログラムです。こんなプログラムを書いてはいけません!
gfortranだと-fcheck=allをつけると、実行時にちゃんと怒られます。
https://qiita.com/mosamosa/items/8945676f358be62ca22c
ということで、コンパイルオプションをちゃんとつけましょう!!
###備考2
さらに興味を持った人から、ポインタ渡しと参照渡しって違うの?って聞かれても、私にはわかりません!!C++とかだと違うらしいです。
https://qiita.com/agate-pris/items/05948b7d33f3e88b8967
詳しい人お願いします!!