2019年1月20日日曜日

2: C++における'リファレンス': 違う!「変数の別名」じゃない!

<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>

「リファレンスとは変数の別名のことである」のようなずさんな説明は葬り去られなければなりません。もしもそれをずさんだと認知できなければ、それは恥ずべきことです。

話題


About: C++

この記事の目次


開始コンテキスト


  • 読者は、C++とはどういうものであるかの基本的な知識を持っている、たとえ、一部の、難しいと考えられている要素を正確に理解していないとしても。

ターゲットコンテキスト



  • 読者は、'リファレンス'がC++において何であるかの1つのリーズナブルな説明を得る。

オリエンテーション


Hypothesizer 7
'リファレンス'がC++において何であるかの多くの説明を私は見つけたが、満足できるものは1つも見つからなかった。

1つの典型的な説明は、「'C++のリファレンス'とは、変数の別名のことである。」といったものだ。...確かに、その説明は次のケースをよく説明する(実際には、この使用法は意義があるようには思われない、なぜなら、その別名を持つ必要性が全く見えないから: なぜ私は、オリジナルの変数を代わりに使わないのだろうか?)。

@C++ ソースコード
int l_integer = 1;
int & l_integerReference = l_integer;

'l_integerReference'というリファレンスが'l_integer'という変数の別名だというのでしょう?...しかしながら、次のコードにて、'a_integerReference'というリファレンスは、...どの変数の別名だというのだろうか?

@C++ ソースコード
#include <iostream>

void referenceArgumentFunction (int const & a_integerReference) {
    ::std::cout << "### 'a_integerReference' is " << a_integerReference << " at " << &a_integerReference << "." << ::std::endl << ::std::flush;
}

int main (int a_argumentsNumber, char const * a_arguments []) {
	referenceArgumentFunction (2 * 3);
	int l_integer1 = 2 * 3;
	::std::cout << "### 'l_integer1' is " << l_integer1 << " at " << &l_integer1 << "." << ::std::endl << ::std::flush;
	int l_integer2 = 2 * 3;
	::std::cout << "### 'l_integer2' is " << l_integer2 << " at " << &l_integer2 << "." << ::std::endl << ::std::flush;
}

'2 * 3'?'2 * 3'は、公式に変数だということになっているのか?...私はそう思わない、以下の理由によって。

第1に、'名前'とは何であるかを明確にしよう。名前とは、それが使用される度に同一のエンティティを代表する表現のことだ。もしも、ある表現が2つの出現で異なるエンティティを代表するのであれば、その表現は名前ではない。

第2に、変数とは名前のついたボックスであるので、名前を持っていないものは変数ではない。

'2 * 3'は複数の出現において異なるエンティティを代表する(私のコンピューターにおける上記コードの結果は、第1の'2 * 3'と第2の'2 * 3'が別々の位置にある2つのデータであることを示す(別のコンピューターでは別の結果になるかもしれない。しかし、少なくとも、3つの'2 * 3'が同一位置にあることはあり得ない))ので、'2 * 3'は名前でなく、'2 * 3'は変数(名前のついたボックス)ではない。

それに、'リファレンス'は、次のケースにおいて別名(別の名前)ではない。

@C++ ソースコード
int l_integer = 1;
int const & l_integerReference = l_integer;

第1のケースと違い、'l_integerReference'というリファレンスには意義があるが、それは、そのリファレンスが、'l_integer'という変数の別の名前ではなく、異なる属性(コンスタントであるという)を持つ別の変数であるからだ。

したがって、前述の説明は本質的に不満足に思われる。

別の典型的な説明は、「'C++のリファレンス'とは、オブジェクトの別名のことである」といったものだ。...ふーむ、その説明は私には意味をなさない。別名は別の名前なので、まず、オリジナルの名前がなければならず、そうして初めて、別名が存在し得る。あるオブジェクト、例えば、"2 * 3"という表現によって生成されるものが名前を持たない(上で論じたとおり)のに、どれがオリジナルの名前なのか?...

それに、先程と同じように、'リファレンス'が名前である旨の説明は不満足だ。

'リファレンス'がファンシーなポインターであるという説明はどうだろうか?...実は、私はかつてその解釈を採用していたという経緯があり、その解釈について後ほど考えてみよう、今はその解釈を私は採用していないのであるが。


本体


1: 注釈


Hypothesizer 7
オフィシャルドキュメントが言っていることを鵜呑みにすることにも、既存の実装がどのようであるかにも、私には関心がない。その理由はある記事で説明されている。私が関心を持っているのは、振る舞い全体を首尾一貫して説明する理論だ(そうした理論は複数あり得る)。


2: 'リファレンス'とは何であるか


Hypothesizer 7
実際には、2つの種類のリファレンスがある: リファレンス変数とリファレンス'ファンクションリターン'。'リファレンス'という用語は両者において同じ意味を持っているが、まず、それらの各々を個別に検討することが役に立つ。

リファレンス変数とは、既存のデータの上にかぶせて定義された変数のことだ。

'別名'のような単語を不用意に使用するのは差し控えよう。別名はただの名前であるが、リファレンス変数とは、ただの名前ではなく、'タイプ'、'スコープ'、'コンスタント性'などの独立した属性群を持つ名前つきボックスだ。

「既存のデータの上にかぶせて定義された」とは何を意味するのか?...リファレンスでない変数は、定義される時、空き領域にアロケートされる。したがって、どこかに存在する既存データをその変数にセットしたい場合、我々はそのデータを、変数であるボックスにコピーする。他方で、リファレンス変数は、定義される時、指定された既存データが占めている場所に定義される、それが、リファレンス変数を定義する際に、あるデータをそのリファレンス変数に関連付けなければならない理由だ。さもなければ、そのリファレンス変数の場所を決定することができない。

リファレンス変数が定義された後でそれを別のデータに関連付けられないというのもまた自然なことだ: それができたら、そのリファレンス変数がその別データの場所に移動されたということを意味するが、C++ではいかなる変数も移動することはできない(「ムーブセマンティクス」なる誤解を招く用語があるが、それにおいて実際には何も移動されない(将来の記事にて「ムーブセマンティクス」を論じる))。

リファレンス'ファンクションリターン'とは、そのファンクションのファンクション呼び出し表現が、リターンステートメントで指定されたデータ(そのデータのコピーではなく)を代表するようになるというメカニズムのことだ。

ここでもまた、'別名'なる単語は当てはまらないことに注意しよう。ファンクションリターンに名前などないし、ファンクション呼び出し表現は名前ではないので、そこには何の名前も関係していない。

結局のところ、リファレンス変数かリファレンス'ファンクションリターン'かによらないどのリファレンスにも共通の特性は、既存データを代表する何かだということだ。


3: 'リファレンス'の有用性


Hypothesizer 7
リファレンス変数が有用でありうる1つの理由は、それが名前ではなく、独自の属性('タイプ'、'スコープ'、'コンスタント性'等)を持った変数であることだ。

実際、1つの変数に複数の名前を持たせても有用には思われない: どの名前を使用しても、同一のタイプ、同一のスコープ、同一のコンスタント性を持つ同一の変数を意味するのだから。

'リファレンス変数'の1つの典型的なユースケースは、ファンクションのリファレンス変数引数だ: リファレンス変数引数は当該ファンクション内スコープを持っているから、そのファンクション内からはアクセスできなかったデータが、そのリファレンス変数引数に代表されることによって、そのファンクション内からアクセス可能になる。

別のユースケースとして、クラスメンバーリファレンス変数が、あるデータを当該クラス内でアクセス可能にする。

しかし、必ずしも、'スコープ'がリファレンス変数が使用される理由だというわけではない: オリジナル変数のコンスタント版を持つということも有用であり得る: それは、ソースファイルの可読性を向上させられる、なぜなら、読み手は基本的に(絶対的にではない、C++では'コンスタント性'は奔放に取り払えるから)そのリファレンス変数に代表されるデータが、そのリファレンス変数を通しては変更されないと安心できる(データの変更をトレースするというのが、ソースファイルを読む際の1つの典型的関心事でしょう?)。

リファレンス'ファンクションリターン'は変数ではないが、その有用性は類似のものだ: 当該ファンクション内で見られるデータをファンクションの呼び出し手が見ることができる。

いずれにせよ、リファレンス(リファレンス変数であれリファレンス'ファンクションリターン'であれ)を使用することの1つの要諦は、既存データを直接使用することだ、そのデータのコピーを不必要に生成することなく。

不必要なコピーを持つことが迷惑なのは、コピーするという動作がコストであるし、コピーがメモリースペースを占有するし、コピーを持つことが、コピー間の一貫性についての疑いと、コピー間の同期を行なう必要性をもたらすからだ。

最後の理由に関して言うと、プログラムを現実のシミュレーションとみなすとき(私は常にそうする)、現実内の単一エンティティに対してプログラム内に単一インスタンスを持つのがリーズナブルで自然だ。


4: 'リファレンス'の必要性


Hypothesizer 7
でも、'リファレンス'を使用してできることは何でも'ポインター'を使用してできるんでしょう?...そう、ある種のシンタックスを気にしなければ。

'リファレンス'が有利な1つのケースは、ある種のオペレーター引数だ。

例えば、もしも、あるクラスの'+'オペレーターの引数が、いわゆる実引数データがコピーされることを避けるためにポインターだったとしたら、'+'オペレーターの右辺にアドレスを指定しなければならないことになるだろう: そうすると、アドレスでないものにアドレスを足しているように見えてしまう。それは、必ずしも間違っているとか悪いとかではないが(オペレーターがファンクションであることを考えると)、'+'オペレーターの直感的フィーリングは壊されてしまう。


5: リファレンス変数の値の生存期間


Hypothesizer 7
どのリファレンス変数も、変数なので、あるスコープを持っている。しかし、そのスコープを通してそのリファレンスの値が有効なわけでは必ずしもない。1つの例を見てみよう。

@C++ ソースコード
#include <iostream>
#include <string>

class Greeter {
	private:
		::std::string const i_name;
		::std::string const & i_nickname;
	public:
		Greeter (::std::string const a_name, ::std::string const & a_nickname);
		void selfIntroduce ();
};

Greeter::Greeter (::std::string const a_name, ::std::string const & a_nickname) : i_name (a_name), i_nickname (a_nickname) {
	::std::cout << "### A Greeter instance is created with the name/nickname, '" << i_name << "'/'" << i_nickname << "'." << ::std::endl << ::std::flush;
}

void Greeter::selfIntroduce () {
	::std::cout << "### I am " << i_name << " also known as " << i_nickname << "!" << ::std::endl << ::std::flush;
}

int main (int a_argumentsNumber, char const * a_arguments []) {
	Greeter l_greeter (::std::string ("Greeter 1"), ::std::string ("Reeter 1"));
	Greeter l_greeted (::std::string ("Greeted 1"), ::std::string ("Reeted 1"));
	l_greeter.selfIntroduce ();
	l_greeted.selfIntroduce ();
}

'::std::string ("Reeter 1")'という表現によって生成されたデータは、その表現が現れたステートメントが終了するまで存在する。そのステートメントの後、'l_greeter'というオブジェクトとそのメンバーリファレンス変数である'i_nickname'は存在するが、そのリファレンス変数の値は有効ではない: アウトプットは私のコンピューターでは妙なものになる(一部のコンピューターではそうでないかもしれない)。


6: 'リファレンス'の安全性


Hypothesizer 7
前セクションで見られる通り、ポインターの代わりにリファレンスを使用することは安全性を保証しない: 無効なメモリーエリアがリファレンスを通してアクセスされることがあり得る。


7: "リファレンス渡し"?


Hypothesizer 7
"リファレンス渡し"?...ふーむ、それは妙な表現に思える。

値渡しは、値が渡される(厳密に言うと、ファンクションの中へコピーされる)ことを意味し、アドレス渡しは、アドレスが渡される(ファンクションの中へコピーされる)ことを意味しているわけでしょう?。それでは、"リファレンス渡し"では、リファレンスが渡される(ファンクションの中へコピーされる)のだろうか?...そんな言明は私には'リファレンス'のいかなる定義においても意味をなさない。私による'リファレンス'の定義においては、リファレンスはファンクション内に新たに作られ(したがって、それは渡されることはできないし、必要もない)、'リファレンス'が別名であるとする定義においては、名前('別名'は'別の名前'という意味だ)が渡されるのだろうか?しかし、名前を渡すというのは有用には思われない、なぜなら、ファンクションは、名前だけ受け取って何をできるのだろうか("a_integerReference"といった名前を各文字毎分析しても、そのファンクションは何らのデータにもアクセスできない)。

"リファレンス渡し"で本当に渡されるものはアドレスであるように思われる。したがって、"リファレンス渡し"は本当にはアドレス渡しなわけだ、何が渡されるかという点においては。ファンクション引数としてリファレンスを使用することとポインターを使用することの違いは、何が渡されるかにはなく(いずれにせよ、アドレスが渡される)、渡されたアドレスがいかに使用されるかにある(そのアドレス地点に変数が定義されるか、そのアドレスがポインターに格納されるか)ようだ。


8: 'リファレンス'はファンシーなポインターとして解釈し得るか?


Hypothesizer 7
実は、以前は、'リファレンス'をポインターの突然変異したものと私はみなしていた、長い間。つまり、リファレンスは、ポインターと同様にアドレスを格納しており、シンタックス上、妙な扱いを受けるだけだ私は考えていた。

その解釈は妥当なのだろうか?...そう思う、それを今は私は採用しないが(上に説明した解釈を採用しているので)。

実際、「C++がシンタックス上、'リファレンス'をそう扱うにすぎない」という説明を受け入れる限り、いかなる動作も説明ができてしまう、おそらくは。

例えば、以下の例では、「"a_argument2 = a_argument1"は本当は'*a_argument2 = *a_argument1'を意味しているのだ、C++はシンタックス上、前者の表現を要求するが。」と言う。

@C++ ソースコード
void swap (int & a_argument1, int & a_argument2) {
	int l_savedArgument2 = a_argument2;
	a_argument2 = a_argument1;
	a_argument1 = l_savedArgument2;
}

それでは、リファレンスの、ポインターとしてのアドレスを取得できない事実('&'オペレーターは、リファレンスに指し示されているデータのアドレスを戻す、リファレンスの、ポインターとしてのアドレスではなく)を私はどう説明するのか?それは、リファレンスがポインターではない証拠ではないのか?...そうでもない。「C++がシンタックス上、'リファレンス'をそう扱うにすぎない」という説明を私は採用したので、私はただ、「C++における'&'オペレーターが、シンタックス上、リファレンスに指し示されているデータのアドレスを戻すものであるにすぎず、C++は、リファレンスの、ポインターとしてのアドレスを取得するいかなるシンタックスも許さないのだ、リファレンスの、ポインターとしてのアドレスが本当はあるにもかかわらず」 と言うだけだ。

何でも奇妙なシンタックスのせいにするそのような説明が望ましいかどうかは、別問題だ。しかしながら、繰り返しになるが、その説明がほとんどの(またはたとえ全てであっても)既存の実装に一致しないという反駁は、私は受け入れない: 実装が仕様を規定することはない(既存の実装すべてと全く違う新たな実装が将来現れる可能性はある、理論的には)。


9: 結びとその先


Hypothesizer 7
今や私は、'リファレンス'が何であるかについての満足のいく説明を得た。

C++には、一般に流布している説明が満足のいくものでない概念が他にもある。将来の記事にて、そうした概念にリーズナブルな説明を行なうよう試みる。"lvalue"および"rvalue"はその内の2つだ。


参考資料


<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>