2019年1月27日日曜日

3: C++における「Lvalue」および「Rvalue」?それらは値じゃない!

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

それらは実体とかけ離れた名称であり、それらについての広く流布された説明が意味をなしていないのは、それらのコンセプトを値として説明しているからです。

話題


About: C++

この記事の目次


開始コンテキスト



ターゲットコンテキスト



  • 読者は、C++におけるいわゆる「lvalue」および「rvalue」が何であるかの1つのリーズナブルな説明を得る。

オリエンテーション


Hypothesizer 7
以下のコードは、「lvalue required as unary ‘&’ operand」のようなコンパイルエラーを引き起こす。

@C++ ソースコード
void pointerArgumentFunction (int * a_integerPointer) {
	*a_integerPointer = 3 * 4;
}

int main (int a_argumentsNumber, char const * a_arguments []) {
	pointerArgumentFunction (&(2 * 3));
}

「lvalue」?...一体、「lvalue」って何だ?...それが正確に何であろうとも、一種のであろうと、人は自然に推測するだろう、だって、名前がl「value」なんだから。

しかしながら、そうでないことを、以下のうまくいくコードが証拠立てている。

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

void referenceArgumentFunction (int const & a_integerReference) {
	::std::cout << "### The address of the temporary object is " << &a_integerReference << ::std::endl << ::std::flush;
}

int main (int a_argumentsNumber, char const * a_arguments []) {
	referenceArgumentFunction ((2 * 3));
}

上記コードがうまくいくのは、"a_integerReference"が「rvalue」ではなく、「lvalue」であるからだ。しかし、上記2コードで問題になっている値は、"2 * 3"という表現で生成される単一のデータ(いわゆるテンポラリーオブジェクト)だ。...なぜ単一のデータが、「rvalue」であり「lvalue」でもあるのか?...変な話だ。

結局判明したことには、「lvalue」も「rvalue」も全然ではなく、表現である。...とても紛らわしい名前だ。それらは、'lexpression'および'rexpression'と呼ばれるべきだと私は思う。

「An rvalue is an xvalue, a temporary object or subject thereof, or a value that is not associated with an object.(rvalueとは、xvalue、テンポラリーオブジェクトまたはそのサブオブジェクト、オブジェクトに関連付けられていない値のいずれかのことである)」といった説明を私は見たが、それは明らかに間違っている: その説明は「rvalue」が値であるかのように語っているが、それは正しくない。実際、上記2番目のコード中の"a_integerReference"はテンポラリーオブジェクトを代表しているが、その表現は「rvalue」ではない。

留意点として、「glvalue」、「xvalue」、「prvalue」という用語もあるが、それでも、いかなる表現も排他的に「lvalue」または「rvalue」であり、したがって、話題を「lvalue」および「rvalue」のみに限定することは理に適っており、それら3つのタイプの表現にはこの記事では立ち入らない(いわゆる「ムーブセマンティクス」(また別の紛らわしい用語)をまず説明しなければならないから)。実際、「ムーブセマンティクス」がなければ、「xvalue」は存在せず、「glvalue」は「lvalue」に一致し、「prvalue」は「rvalue」に一致し、「lvalue」と「rvalue」だけが考慮すべきものとなる。

ところで、「lvalue」と「rvalue」の区別は本当に必要なのだろうか?...あるセクションにてそれについて考察しよう。


本体


1: 「rvalue」とは何か?


Hypothesizer 7
第1に、いわゆる「rvalue」は、ではなく、表現である。言い換えると、「rvalue」は、表現の属性であって、データの属性ではない。実際、私は代わりに'rexpression'という用語を使用する。

上記'オリエンテーション'にて挙げたコードを再度見てみよう。

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

void referenceArgumentFunction (int const & a_integerReference) {
	::std::cout << "### The address of the temporary object is " << &a_integerReference << ::std::endl << ::std::flush;
}

int main (int a_argumentsNumber, char const * a_arguments []) {
	referenceArgumentFunction ((2 * 3));
}

"2 * 3"および"a_integerReference"という表現は、それぞれ、rexpression、lexpressionである、両者は、同一のテンポラリーオブジェクトを代表しているのであるが。

「テンポラリーオブジェクトはrvalueである」といった説明が広く流布しているが、それが誤りであることを、上記コードが明確に証明している。実際には、正しい言明は、「テンポラリーオブジェクトを生成する表現はどれもrexpressionである」である。

私の考える'rexpression'の正しい定義は、'テンポラリーオブジェクトを生成する任意の表現または明示的にrexpressionにされた任意の表現'だ。

「明示的にrexpressionにされた」?...そう、いわゆる「ムーブセマンティクス」の導入に伴って、任意の表現をrexpressionにする方法が導入された。実際、「ムーブセマンティクス」を除外すれば、上記定義の前半だけで十分だ。


2: 「lvalue」とは何か?


Hypothesizer 7
ここでもまた、いわゆる「lvalue」は、ではなく、表現である。実際、私は代わりに'lexpression'という用語を使用する。

'lexpression'の定義は、'rexpressionではない任意の表現'だ。


3: rexpressionsおよびlexpressionsの例


Hypothesizer 7
rexpressionおよびlexpressionの例をいくつか見てみよう。注意として、値のアドレスを取得できるか否かはlexpressionであるかrexpressionであるかのリトマス試験として使え、rexpressionの値のアドレスを取得しようとするステートメントはどれもコンパイルエラーを引き起こす。

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

			double const & lexpressionReturnFunction (double const & a_doubleReference) {
				return a_doubleReference; // The return is really the value of "a_doubleReference".
			}
			
			float prexpressionReturnFunction () {
				float l_float = 1.0;
				return l_float; // The return is not really the value of "l_float", but a copy of the value of "l_float".
			}
			
			double && xexpressionReturnFunction (double & a_doubleReference) {
				return ::std::move (a_doubleReference); // The return is really the value of "a_doubleReference", but any call of this function is explicitly turned into an rexpression by virtue of the '::std::move'.
			}
			
			void checkExpressionTypes () {
				int l_integer = 1;
				int * l_integerPointer = &l_integer;
				int const & l_integerReference = l_integer;
				double l_double = 2.0 * 3.0;
				::std::cout << "### The address of 'l_integer' is '" << &l_integer << "'." << ::std::endl << ::std::flush; // an lexpression
				::std::cout << "### The address of 'l_integerPointer' is '" << &l_integerPointer << "'." << ::std::endl << ::std::flush; // an lexpression
				::std::cout << "### The address of '*l_integerPointer' is '" << &(*l_integerPointer) << "'." << ::std::endl << ::std::flush; // an lexpression
				::std::cout << "### The address of 'l_integerReference' is '" << &l_integerReference << "'." << ::std::endl << ::std::flush; // an lexpression
				//::std::cout << "### The address of 'l_integer * 2' is '" << &(l_integer * 2) << "'." << ::std::endl << ::std::flush; // an rexpression
				::std::cout << "### The address of '*((int *) 1)' is '" << &*((int *) 1) << "'." << ::std::endl << ::std::flush; // an lexpression, although bad (although "*((int *) 1)" does not represent any valid datum, it is still an lexpression: an explanation like "a value that is not associated with an object is a rvalue" is wrong)
				::std::cout << "### The address of 'checkExpressionTypes' is '" << (void *) &checkExpressionTypes << "'." << ::std::endl << ::std::flush; // an lexpression
				::std::cout << "### The address of 'lexpressionReturnFunction (l_double)' is '" << &(lexpressionReturnFunction (l_double)) << "'." << ::std::endl << ::std::flush; // an lexpression
				::std::cout << "### The address of 'lexpressionReturnFunction (2.0 * 3.0)' is '" << &(lexpressionReturnFunction (2.0 * 3.0)) << "'." << ::std::endl << ::std::flush; // an lexpression, although an expression that represents a temporary object
				//::std::cout << "### The address of 'prexpressionReturnFunction ()' is '" << &(prexpressionReturnFunction ()) << "'." << ::std::endl << ::std::flush; // an rexpression
				//::std::cout << "### The address of 'xexpressionReturnFunction ()' is '" << &(xexpressionReturnFunction (l_double)) << "'." << ::std::endl << ::std::flush; // an rexpression
				::std::cout << "### The address of const_cast <int &> (l_integer)' is '" << &(const_cast <int &> (l_integer)) << "'." << ::std::endl << ::std::flush; // an lexpression
				//::std::cout << "### The address of ::std::move (l_integer)' is '" << &(::std::move (l_integer)) << "'." << ::std::endl << ::std::flush; // an rexpression
			}

上記コードの"::std::move"は、各lexpressionを明示的にrexpressionにしている(次記事にて論じられる)。


4: 'lexpression'と'rexpression'をなぜ区別しなければならないのか?


Hypothesizer 7
それにしても、'lexpression'と'rexpression'をなぜ区別しなければならないのだろうか?...うーん、その必要はない、と私は思う。

歴史的に言うと、それらが区別されているのは、一部の表現が代入の左辺で使用されるのを禁止するためだ。

例をいくつか見てみよう。

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

				int l_integer1 = 1;
				int l_integer2 = 2;
				//l_integer1 * l_integer2 = 3;
				::std::string ("aaa") = ::std::string ("bbb");

"l_integer1 * l_integer2 = 3;"というステートメントはコンパイラーが許さないが、そのステートメントは本当に、意味をなしていないだろうか?...私はそうは思わない。そのステートメントが意味するのは、あるテンポラリーオブジェクトがどこかに(場所がCPUレジスターの中であったとしても別に構わない)生成され、その場所に'3'が入れられるということだ。意味をなしている。

確かに、そのステートメントは役に立つわけではないかもしれないが、他の多くの役に立たないステートメント("l_integer1 = l_integer1;"のような)はC++で許されており、ある種の役に立たないステートメントのみを禁止しなければならない必要性が私には見えない: 実際、なぜ、コンパイラーに最適化の一環として役に立たないステートメントを黙って除去させないのか、プログラマーを「lvalue」や「rvalue」についてのエラーメッセージで煩わせることなしに?

rexpressionに対するアドレス取得オペレーションが全て禁止されるという制限もあるが、私の意見ではその制限も不必要だ。論理的には、rexpressionであることは、その表現に対するアドレス取得オペレーションを禁止にすることを、全然必要付けない。趣旨が、コンパイラーに一部のテンポラリーオブジェクトをCPUレジスターのみに格納することを許すということにあるのであれば、最適化の一環として、コンパイラーに、何に対してメモリー割り当てを安全に免除できるかを判断させればよいだけだ(あるテンポラリーオブジェクトのアドレス取得が試みられていれば、コンパイラーにそれをメモリーに格納させ、そうでなければ、コンパイラーはテンポラリーオブジェクトをCPUレジスターのみに格納してもよい): あまり意味のある結果をもたらさない(アドレスは、そのテンポラリーオブジェクトをリファレンス引数を介してアクセスする任意のファンクション内で取得できてしまう、結局のところ)のに、全てのrexpressionに対する全てのアドレス取得オペレーションを一律に禁止することがリーズナブルだとは私は思わない。

テンポラリーオブジェクトのアドレスをファンクションに渡す(引数を変更可能にする目的のために)のは役に立たない、なぜなら、変更がファンクションコール後に見えないから、と言う人がいるかもしれないが、私は言う、ポインター引数を持つファンクションがいくつかあって、それらの1つに、あるテンポラリーオブジェクトのアドレスを渡さなければならないのだ、変更がこのコールに対して見ようが見えまいが(引数がポインターであることには意味がある、なぜなら、そのファンクションは、別に、テンポラリーオブジェクト指定で呼ばれるためだけのものではないから: 非テンポラリーオブジェクト指定で呼ばれることもある)、と。

確かに、私はコンパイラー開発者ではないから、一部のコンパイラーの開発者がそうした制限を欲する何らかの理由があるのかもしれないが、プログラマーたちが納得できる説明があってしかるべきだ、もしそうした理由があるのであれば(私は見つけられていない、今のところ)、と私は主張する。


5: 結びとその先


Hypothesizer 7
これで、「lvalue」および「rvalue」が何かを理解したようだ。

それら2つを区別する必要性は理解できないが、その区別に基づいたいくつかの制限が故に、ある種のコードが許可されないという現実に私は向き合わなければならない。「lvalue」や「rvalue」についてのエラーをコンパイラーが出してくるので、それらが何であるかを私は理解しなければならない。

実は、いわゆる「ムーブセマンティクス」(また別の紛らわしい用語)が導入されたために、rexpressionは別の結果ももたらすようになった。「ムーブセマンティクス」が何であるか、また、「lgvalue」、「xvalue」、「prvalue」が何であるかを次記事で研究しよう。


参考資料


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