2019年7月14日日曜日

6: C++における'コンスタントメソッド: 一般的な説明の半分は誤っている

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

コンスタントメソッドの定義は、「当該オブジェクトの状態を変更しないメソッド」ではありません。そこでの取り違えが一部の混乱の原因になっています。

話題


About: C++

この記事の目次


開始コンテキスト



ターゲットコンテキスト



  • 読者は、'コンスタントメソッド'が本当には何であるか、およびリーズナブルでないある制限を回避する方法を理解する。

オリエンテーション


Hypothesizer 7
以下のコードはコンパイルできない、「binding reference of type ‘int&’ to ‘const int’ discards qualifiers」というエラーが出て。

@C++ ソースコード
class Test1 {
				private:
					int i_integer1;
				public:
					int & constantMethod1 () const;
			};

			int & Test1::constantMethod1 () const {
				return i_integer1;
			}

はあ?...なぜ?...'コンスタントメソッド'とは'当該オブジェクトの状態を変更しないメソッド'のことだと習った(いくつかのチュートリアルで)のだが、間違いなく、そのメソッドはその基準を満たしている...

そのエラーメッセージは、'Test1::constantMethod1'中の「i_integer1」はコンスタントであり、そのコンスタントを非コンスタントなリファレンスリターンは参照できない、と述べている。

うーん...、そこには2つ問題がある: 1) その言明は妙だと思われる、2) 'コンスタントメソッド'の実情が「当該オブジェクトの状態を変更しないメソッド」という叙述に合っていない。

それはさておき、私はどうするように想定されているのだろうか?

あるQ & Aサイトのいくつかの回答は、リターンタイプはコンスタントリファレンス(本ケースでは、'int const &')でなければならないと主張している。...そう言われても、それは、私には非コンスタントなリファレンスでなければならない: 'i_integer1'の内容は、後で変更されるのだ。

それらの回答は、もしも、リターンが非コンスタントリファレンスであったら、'Test1'オブジェクトの状態が後で変更できてしまうだろうと主張している。...だから何です?...それが私がまさに欲していることであって、それは、そのコンスタントメソッドが当該オブジェクトの状態を変更しないということに全然、違反していないでしょう?...私はただ、そのメソッド自身が当該オブジェクトの状態を変更しないことを確実にしたいだけであって、オブジェクトが後で変更されることは私にとって全く問題ないことだ。

結局、判明したのは、「'コンスタントメソッド'とは'当該オブジェクトの状態を変更しないメソッド'のことだ」のような説明が不適切だということだ。私が言っているのは、その説明は、'コンスタントメソッド'の1側面の叙述としては間違っていない(確かに、'コンスタントメソッド'は当該オブジェクトの状態を変更しない)が、「'コンスタントメソッド'とは何であるか」への答えとしては不適切であるということ、なぜなら、その説明は、'コンスタントメソッド'の本質を表わしていない。

確かに、世の中には、適切な説明も多く見かけるが、流布している説明のおよそ半分(厳密な統計値ではない)は、基本的に、あの不適切な説明と同等のようだ。

両タイプの説明を見かけていたのだが、私は不適切なものに飛びついたのだった、それが、私の欲するものを表わしていたから: 実のところ、私が欲しかった(そして今も欲しい)のは、当該オブジェクトの状態を変更しないメソッドなのだが、不運なことに、'コンスタントメソッド'はそれではない。


本体


1: 'コンスタントメソッド'とは本当には何であるか


Hypothesizer 7
実際には、'コンスタントメソッド'とは、'コンスタントなオブジェクトに対して安全に呼べるメソッド'のこと(「オブジェクト」という用語に注意)であり、'当該オブジェクトの状態を変更しないメソッド'のことではない、定義としては。

少なくとも、それが、'コンスタントメソッド'がいかに振る舞うかを本当に表わしている説明だ。

その正確な理解に基づいてのみ、上記コードが許されないかを理解できる: メンバーデータのコンスタントでないリファレンスをリターンすることは、当該オブジェクトがコンスタントである時に安全ではない。


2: 不運なことに、C++には、'当該オブジェクトの状態を変更しないメソッド'などというものはない


Hypothesizer 7
ふーむ...、'コンスタントなオブジェクトに対して安全に呼べるメソッド'の有用性を私は絶対的に否定しはしない: コンスタントなオブジェクトを変更すると、規定されていない振る舞いに至る可能性があるため、そうした可能性を事前に排除したいわけでしょう?

しかしながら、ほとんどのケースにおいて、私が欲しいのは、'当該オブジェクトの状態を変更しないメソッド'なのだ。

不運なことに、C++にはそのようなものはない。


3: 問題は、コンスタントオブジェクトを表わさない任意のコンスタント表現に対してメソッドが呼ばれる時に、そのメソッドがコンスタントでなければならない、とC++が求めることだ


Hypothesizer 7
'コンスタントメソッド'とは、'コンスタントなオブジェクトに対して安全に呼べるメソッド'のことなので、コンスタントオブジェクトに対してメソッドが呼ばれる時に、そのメソッドがコンスタントでなければならない、とC++が求めることは、とてもリーズナブルだ。しかし、コンスタントオブジェクトを表わさない任意のコンスタント表現に対してメソッドが呼ばれる時にも、そのメソッドがコンスタントでなければならない、とC++は求めるのであり、それが、とても非リーズナブルなわけ。

その違いを理解していない人がいるのであれば、以下のコードがそれを明確にするだろう。

@C++ ソースコード
class Test1 {
				private:
					int i_integer1;
				public:
				    // This has to be, unreasonably, a constant method because this is called on the constant expression argument in 'Test2::constantMethod2'; the return type has to a constant reference because the definition of 'constant method' requires so.
					int const & constantMethod1 () const;
			};
            
			int const & Test1::constantMethod1 () const {
				return i_integer1;
			}
			
			class Test2 {
				public:
				    // The return type has to be a constant reference because 'a_test1.constantMethod1 ()' returns a constant reference.
					int const & constantMethod2 (Test1 const & a_test1) const;
			};
			
			int const & Test2::constantMethod2 (Test1 const & a_test1) const {
				return a_test1.constantMethod1 ();
			}
			
int main (int a_argumentsNumber, char const * a_arguments []) {
    Test1 l_test1;
    Test2 const l_test2;
    int & l_integer1 = l_test2.constantMethod2 (l_test1); // This is not allowed, of course . . .
    l_integer1 = 2;
}

'コンスタント表現'は、表わされたデータ(本ケースでは、オブジェクト)がその表現を通しては変更できない(データがほかの方法で変更されることは問題ない)ことを意味しており、'Test2::constantMethod2'内の'a_test1'は、まさに、非コンスタントオブジェクトを表わすコンスタント表現である('main'内の'l_test1'は、非コンスタントだ)。

'a_test1'によって表わされたオブジェクトが'Test2::constantMethod2'の外で変更されることには全く問題がないのだが、C++は、非リーズナブルにも、'a_test1'によって表わされたオブジェクトはコンスタントオブジェクトに違いないと決めつけるのだ、誤って。

そのずさんな判断が、'Test1::constantMethod1'がコンスタントメソッドであるように要求し、それが、そのリターンタイプがコンスタントリファレンスであるように要求し、それが、'Test2::constantMethod2'のリターンタイプがコンスタントリファレンスであるように要求し、それが、'l_integer1'を非コンスタントリファレンスにしようとする私の意図をくだくのだ。


4: もっと一般的に言って、C++標準による'コンスタント性'の扱いはずさんだ


Hypothesizer 7
実のところ、もっと一般的に言って、C++標準による'コンスタント性'の扱いはずさんだ: C++標準は、'データがコンスタントであること'と'表現がコンスタントであること'を弁別しない。

'オリエンテーション'で挙げたエラーメッセージの主張が変なのは、'Test1::constantMethod1'内の"i_integer1"がコンスタントなのは表現のコンスタント性であるのだが(その表現がコンスタントなのは、コンスタントインスタンスメソッド内で'this'がコンスタントであるからであることは知っている)、リファレンスはデータに結びつけられるのであって表現にではないのであり、結びつけられる対象のデータは非コンスタントだからだ。


5: 'const_cast'は、一部の人々が主張しているよりも多くのケースで妥当であるようだ


Hypothesizer 7
一部の人々は、'const_cast'を使うべきでない(少なくとも、コンスタントセーフでない既存ライブラリがそれを必要化する場合を除いては)と強固に主張しているが、一定のケースでは、'const_cast'がベストなオプションであるようだ。

つまり、C++標準が不満足かつ非リーズナブルなのであり、その不満足性・非リーズナブル性を、それらが現れるところで埋め合わせる方が、それらに我慢して何らかの迂遠な回避策をでっち上げるよりも良いように思われる。

上記コードでは、'コンスタントメソッド'の公式な定義を受け入れることを前提にすれば、'a_test1'はコンスタントオブジェクトを表わしていないのに'Test1::constantMethod1'がコンスタントでなければならないというところに非リーズナブル性が現れる。そこで、その非リーズナブル性を埋め合わせる1つの方法は、'Test1::constantMethod1'のコンスタント性を取り除いて、'Test1::constantMethod1'を呼べるようにするために、'a_test1'に'const_cast'を使用することだろう、以下のように。

@C++ ソースコード
class Test1 {
				private:
					int i_integer1;
				public:
					int & constantMethod1 ();
			};
            
			int & Test1::constantMethod1 () {
				return i_integer1;
			}
			
			class Test2 {
				public:
					int & constantMethod2 (Test1 const & a_test1) const;
			};
			
			int & Test2::constantMethod2 (Test1 const & a_test1) const {
				return (const_cast <Test1 &> (a_test1)).constantMethod1 ();
			}
			
int main (int a_argumentsNumber, char const * a_arguments []) {
    Test1 l_test1;
    Test2 const l_test2;
    int & l_integer1 = l_test2.constantMethod2 (l_test1);
    l_integer1 = 2;
}

他方では、'コンスタントメソッド'の公式な定義を拒否して、'当該オブジェクトの状態を変更しないメソッド'であるという'コンスタントメソッド'のオリジナルな定義を採択することを前提にすれば、'Test1::constantMethod1'が'i_integer1'への非コンスタントリファレンスを返せないというところに不満足性が現れる。そこで、その不満足性を埋め合わせる1つの方法は、'Test1::constantMethod1'の"i_integer1"に'const_cast'を使用して、"i_integer1"のコンスタント性を取り除いて、'i_integer1'への非コンスタントリファレンスを戻すことだろう、以下のように。

@C++ ソースコード
class Test1 {
				private:
					int i_integer1;
				public:
					int & constantMethod1 () const;
			};
            
			int & Test1::constantMethod1 () const {
				return const_cast <int &> (i_integer1);
			}
			
			class Test2 {
				public:
					int & constantMethod2 (Test1 const & a_test1) const;
			};
			
			int & Test2::constantMethod2 (Test1 const & a_test1) const {
				return a_test1.constantMethod1 ();
			}
			
int main (int a_argumentsNumber, char const * a_arguments []) {
    Test1 l_test1;
    Test2 const l_test2;
    int & l_integer1 = l_test2.constantMethod2 (l_test1);
    l_integer1 = 2;
}

いずれにせよ、'const_cast'を使うのは、C++標準がずさんだからなのである。


6: リーズナブルであることよりも安全であることが優先する?


Hypothesizer 7
表現がコンスタントであるだけでデータもコンスタントであると決めつけるのはリーズナブルでない: そのような言いがかりは、偏見であり頑迷である。

しかし、そのほうが安全?...'表現のコンスタント性'を適切に執行することが技術的に困難であるという前提に立てば、そうかもしれない、C++のコンスタント性機能の非リーズナブルさ加減に愛想を尽かしてその機能全体を放棄するという判断をする人が出てこないならば。

しかし、一体いつからC++は安全になったのだろうか?私の意見では、それは常に、本質的に不安全であった。それは常に、プログラマーが自分のやっていることを知り注意深くあることを要求するプログラミング言語であった。

私からすると、ある措置がリーズナブルかつ安全であるならば、勿論、それは結構であるが、リーズナブル性を犠牲にして安全であろうとするのは、一般的に言っても、賛成しかねるし、特に、プログラマーがどのみち不安全なことをできるプログラミング言語では場違いに思われる。

C++標準は妙な方向に向かっていると私は思う。


7: 結びとその先


Hypothesizer 7
これで、'コンスタントメソッド'が本当には何であるかを理解したようだ: 'コンスタントなオブジェクトに対して安全に呼べるメソッド'のことであって、'当該オブジェクトの状態を変更しないメソッド'のことではない。

しかし、私は本当に'当該オブジェクトの状態を変更しないメソッド'が欲しく、また、任意のコンスタント表現にも'コンスタントメソッド'を使うように要求される、非リーズナブルなことに。

したがって、C++標準のその不満足性と非リーズナブル性を埋め合わせるために、'const_cast'には、使用するに適した場所があるようだ、どんなに強固に一部の人々がそれを使うことを非難しようとも。

C++について不満足な説明が他にもいくつかあるので、もっとリーズナブル説明を行なうよう将来の記事にて試みよう。


参考資料


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