Level 3:関数 3.2 参照による呼び出し |
※資料は自著より引用
ここでは,「参照による呼び出し」を題材に,C++で新しく登場したリファレンス(reference, 参照)を紹介する。
■3.2 参照による呼び出し
●関数に実引数を渡す2つの方法
前期に学習したように,関数を呼び出す際に引数を渡す方法には以下の2種類があった。
(1) 値による呼び出し
(2) 参照による呼び出し
以下の例 List 3-6 は,階乗計算を行う関数factの定義例。ここでは,第2引数をアドレス渡しにして計算結果を返している。
なお,階乗計算とは,「5 × 4 × 3 × 2 × 1」のように,特定の整数値から1ずつ減らして1まで掛け合わせることを言う。
上記の例は「5の階乗」で「5!」と略記する。
List 3-6 callby.c
●リファレンス
C言語ではポインタを使ったアドレス渡しによって擬似的に「参照による呼び出し」を実現していたが,
C++では,リファレンスという新しい考え方で,真の「参照による呼び出し」を実現している。
リファレンスとは,「変数や関数の別名(ニックネーム)を作って利用する」という考え方である。例えば,
「情報太郎」君に「たっくん」とか「たーくん」というようなニックネームをつけて呼ぶようなものである。
C++ ではこのニックネームに相当するリファレンス名を,下図Fig. 3-6 の書式で作る(宣言する)ことができる。
リファレンスを宣言するときに指定する型( &の左側の型)は,オリジナルの型と同じでなければならない事に注意。
●リファレンスの振る舞い
実際に,リファレンスを作成・利用した例を List 3-7 に示す。
10行目で変数aのリファレンスrを宣言しているが,それ以降,リファレンスrはオリジナルであるaの事を
表している。 リファレンスのリファレンスを作っても,やはりオリジナルの別名となる(18〜22行目)。
39〜40行目は,
5行目定義した関数のリファレンスを作成して利用している例である。
List 3-7 ref.cpp
●リファレンスの仕組み
リファレンスは,どのような仕組みで実現されているのだろうか。リファレンス名は,オリジナルの変数や関数
の別名であるので,状況が許せば単にコンパイル時にリファレンス名をオリジナルの名前に置き換えればいい。
List 3-7 では,コンパイラがリファレンス名rをオリジナルの名前aに書き換えコンパイルしてしまえばいいのである。
しかし,冒頭で紹介した「参照による呼び出し」に関わる場合は,実はそう簡単にはいかない。そのような場合,
コンパイラはリファレンスを Fig. 3-7 のように実現する。つまり,オリジナルを指し示すポインタ値を格納する
ポインタ変数を用意するのである。これがリファレンスの正体なのだ。そして,ややこしいポインタの記法を使
わずにオリジナルを利用できる様にしているのである。
●const リファレンス
実は,
・
「定数」や「計算式(の計算結果)」のリファレンス
・オリジナルの型と厳密には同じでは無いが互換性のある方の変数・関数・定数・計算式(の計算結果)
のリファレンスも作ることができる。この場合は,Fig. 3-8 のように,const なリファレンスとして宣言する。
もちろん,constなので,リファレンスが表しているオリジナルの変更は禁止される。
このような場合,リファレンスはconstでなければならないのだろうか。
(a)オリジナルが「定数」や「計算式(の計算結果)」の場合:
この場合は,オリジナル自体が変更不可能な物なのでリファレンスの型もconstとしなければならないのは当然である。
(b)
オリジナルが「オリジナルの型と厳密には同じでは無いが互換性のある型の変数・関数・定数・計算式(の計算結果)」の場合
例えば,float型はdouble型の変数へ代入できる。これは,float型→double型の型変換が可能である,つまり互換性が
ある型なので,Fig 3-8 に従って
float f = 1.0f;
const double & cr = f;
という具合にリファレンスを作成できる。状況としては,リファレンスcrの型はdouble型だし,オリジナルfの型はfloat型で
ある。このとき,リファレンスcrを利用しているコード側からは,crの値はあくまでdouble型として扱えなくてはならない(A)。
しかし,オリジナルfはfloat型なのだ。double型とfloat型は,表現できる値の細かさ(精度)や絶対的な値の範囲がことなり,
while( 1 ) { g = g / 3.0; }
というような場合でも,繰り返しが進むうちにgがdouble型かfloat型かでgの値が変わってくる。
float型をより高精度・より広い表現範囲を持つdouble型に変換することには問題は無い(失われる情報は無い)ものの,
double型とfloat型はまったく同じ様に振る舞う(変化する)とは限らないのだ(B)。
この(A)と(B)の矛盾を解決するために,C++では下図 Fig.3-8 のような工夫をしている。まず,リファレンスと同じ型の
無名の変数(インスタンス)を用意し,オリジナルの値をそこへ代入する。その無名の変数(インスタンス)へのポインタ値を
格納した変数をリファレンスとして使う。これでリファレンスが示す先(無名の変数(インスタンス))の型は同じ型となる。
しかし,そのままでは(B)で示した値の変化の違いは解決しない。そこで,(B)のような問題を起こさないようにするため,
リファレンス自体の値の変更をconst指定によって禁止するのである。
●リファレンス引数
仮引数としてリファレンスを使うと,真の「参照による呼び出し」が可能になる。しかも,アドレス渡しを使った場合
よりも関数内の記述が綺麗になる(ポインタ特有の気泡を使わなくても良いため)。
List 3-8に,List 3-6を真の「参照による呼び出し」を行うように書き換えた例を示す。関数factの第2仮引数が
long & r
というように,リファレンス型になっていることに注意。このレファレン型仮引数は呼び出されるたびに実引数によって
初期化される。つまり,関数factの中では,リファレンス仮引数rは実引数(この場合は変数result)そのものを表している
ことになる。関数factの中でリファレンス仮引数rを変更することは,実引数resultの内容を変更することに等しいのである。
List 3-6 callbyr.cpp
●リファレンス引数の注意点
リファレンスを仮引数に使う方法を知ったからと言って,関数の仮引数全てをリファレンスにすれば良いという
わけでは無い。それぞれの用途と効率を考えて,仮引数を
・普通の変数にする(値による呼び出し)
・ポインタ変数を使ったアドレス渡し(擬似的な参照による呼び出し)
・リファレンスを使った参照渡し(参照による呼び出し)
のいずれにするか考えること。下表Table 3-1 に各場合の効率をあげる。
●標準的な引数の渡し方
引数の渡し方を考える際の標準的なチャートを下図 Fig. 3-10に示す。
●組み込み型でアドレス渡しを利用する理由
上図 Fig. 3-10 では,
実引数の値を変更する場合は,組み込み型ならアドレス渡しを利用するようになっている。
なぜリファレンス渡しにしないのだろうか。これは,C言語時代から変数aが組み込み型の変数なら, f( &a ) という
呼び出しが書かれているなら,これがアドレス渡しであることが一目で分かり,変数aの値が変更される可能性があ
ることがわかる。それに対し,f( a )と書いてあれば,C言語なら値渡しで,変数aの値は変更されないと判断できた。
そうした有益な経験則があるところに,「f( a ) は実はリファレンス渡しで,変数aの値は変更される可能性があり
ます!」という新たな状況が生まれるのは,上手くないのである。
●関数設計時の引数渡しのポイント
また,実効的な実引数が存在しないような種類の引数はリファレンス渡しでは無く,アドレス渡しを使おう。ポインタ
値なら実効的な実引数が無いときは空ポインタ値を渡せば良い。そもそも,リファレンス仮引数は実引数による初期化が
必要,つまり,必ず実効的な実引数が存在している必要がある。
ポインタ仮引数をリファレンス仮引数に書き換える動機の一つは,関数内の処理記述の綺麗さだろう。しかし,配列引数
の場合は,リファレンス仮引数を導入するまでもなく,関数外の配列操作と,仮引数で受け取った配列の操作は同じ形に
なる(List 3-9)。これは,前期で学習したとおり,配列操作の記法そのものがポインタを使った記法の別の形であるためだ。
他にも理由があるが,配列引数もリファレンスを使わない方が良い。
●リファレンス型の返却値
リファレンスのおもしろい使い方に関数の返値をリファレンス型にするテクニックがある。List 3-10に
使用例を示す。関数fの返り値は大域配列gMesのいずれかの要素へのリファレンスになっている。例えば,
f( 0 ) はg[ 0 ] のリファレンスになっているので,f( 0 ) = 's'; という書き方が可能で,この場合,gMes[ 0 ]
の値が's'となる。List 3-10 の場合は,12行目の処理で,f( 4 ) は gMes[ 4 ] のリファレンスになっているので,
gMes[ 4 ] の文字は,'!'から'o'に書き換えられることになる。
●リファレンスを返す関数の注意点
上例のように返り値をリファレンス型にするのはテクニック的にはおもしろいが,
『局所変数へのリファレンスを返してはいけない』
ということは憶えておこう。局所変数は関数が呼ばれる度に生成され,関数が終了する度に消滅する。局所変数への
リファレンスを返り値として返しても,その返り値を受け取った側からしてみれば,受け取ったリファレンスのオリジナル
は消滅済みである。その場合,帰ってきたリファレンスのオリジナルは,消えた局所変数が存在していたメモリ上の
スペースで,そこは別の目的で使用されているかもしれない。