''const'はそれをコンスタントにする'のような浅はかな理解では上手くいきません。'const'には、様々な意味があります。
話題
About: C++
この記事の目次
- 開始コンテキスト
- ターゲットコンテキスト
- オリエンテーション
- 本体
- 1: こうした広く流布している説明では上手くいかない
- 2: 'const'を私は使うべきか?
- 3: 用いるべき記法
- 4: 様々な'const'たち
- 4-1: 任意のノーマル変数に作用するとき
- 4-2: On Any Pointer 任意のポインターに作用するとき
- 4-3: 任意のリファレンスに作用するとき
- 4-4: 任意の配列に作用するとき
- 4-5: 典型的なコンテナに作用するとき
- 4-6: 任意のファンクションリターンに作用するとき
- 4-7: 任意のクラスインスタンスメソッドに作用するとき
- 5: 様々な'const'に対処する
- 5-1: 用語体系
- 5-2: 'const'変数を初期化する
- 5-3: どの変数に何がセットできるか
- 5-4: どのリターンタイプとして何がリターンできるか
- 5-5: C++標準がアンリーズナブルな時
- 5-6: 'const_cast'を使用して'const'性を取り除く
開始コンテキスト
- 読者は、C++の基本的知識を持っている。
- 読者は、'データ'、'変数'、'表現'、'値'とは何であるかの正確な知識を持っている。
- 読者は、'通常変数'、'ポインター'、'リファレンス'とは何であるかの正確な知識を持っている(より図解的な説明が別のシリーズのある記事にある)。
ターゲットコンテキスト
- 読者は、C++で任意の'const'を取り扱う方法を知る。
オリエンテーション
'const'の様々な意味が十分に説明されていない、一般的には。
C++標準は常にリーズナブルであるわけではない。
本体
1: こうした広く流布している説明では上手くいかない
Hypothesizer 7
あるクラスを考えてみよう。
theBiasPlanet/coreUtilitiesTests/constantsTest2/Test1Test.hpp
@C++ ソースコード
#include <string>
using namespace ::std;
namespace theBiasPlanet {
namespace coreUtilitiesTests {
namespace constantsTest2 {
class Test1Test {
private:
static void test ();
static void method0 (string const & a_string0, string & a_string1);
};
}
}
}
theBiasPlanet/coreUtilitiesTests/constantsTest2/Test1Test.cpp
@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/constantsTest2/Test1Test.hpp"
#include <iostream>
namespace theBiasPlanet {
namespace coreUtilitiesTests {
namespace constantsTest2 {
void Test1Test::test () {
~
}
void Test1Test::method0 (string const & a_string0, string & a_string1) {
cout << "### 'a_string0' at the start: " << a_string0 << endl << flush;
a_string1 += " P.S. Do not forget.";
cout << "### 'a_string0' at the end : " << a_string0 << endl << flush;
}
}
}
}
その'const'は何を意味しているのだろうか?
広く流布している説明の1つは、「そのリファレンスによって参照されているデータはコンスタンスである」というものだ。
いいや、それは正しくない。'Test1Test::test ()'のある実装を考えてみよう、以下のように(それは、完全に正当である)。
@C++ ソースコード
void Test1Test::test () {
{
cout << "### Revealing the sloppiness of prevalent explanations Start" << endl << flush;
string l_string0 {"a string"};
string l_string1 {"another string"};
method0 (l_string0, l_string1);
cout << "### Revealing the sloppiness of prevalent explanations End" << endl << flush;
}
}
@出力
### Revealing the sloppiness of prevalent explanations Start
### 'a_string0' at the start: a string
### 'a_string0' at the end : a string
### Revealing the sloppiness of prevalent explanations End
そのリファレンスに参照されているデータは'l_string0'の値であり、それは全然コンスタンスではない。
そういうずさんな説明をするのは控えましょう。
別の広く流布している説明は、「そのリファレンスに参照されているデータは、そのリファレンスの生存期間中変化しないことが保証されている」というものだ。
ファイナルアンサー?. . .えーと、'Test1Test::test ()'の実装を変えてみよう、以下のように(これも、完全に正当である)。
@C++ ソースコード
void Test1Test::test () {
{
cout << "### Revealing the sloppiness of prevalent explanations Start" << endl << flush;
string l_string0 {"a string"};
string l_string1 {"another string"};
method0 (l_string0, l_string0);
cout << "### Revealing the sloppiness of prevalent explanations End" << endl << flush;
}
@出力
### Revealing the sloppiness of prevalent explanations Start
### 'a_string0' at the start: a string
### 'a_string0' at the end : a string P.S. Do not forget.
### Revealing the sloppiness of prevalent explanations End
それは全然、変化しないことが保証されていない!
そのコードがあまりにもわざとらしいというのであれば、そのデータは、別のスレッドから正当に変更できる。
そういうずさんな説明をするのは控えませんか?
そういうずさんな説明たちの問題点は、各'const'がそこにあるべきか否かの適切な判断を阻害することだ: 我々は判断を、その'const'の意味の理解に基づいて行なうのだから、適切な判断を行なうためには、その理解が正確である必要がある。
2: 'const'を私は使うべきか?
Hypothesizer 7
そもそも、なぜ、私は'const'にかかずりあわないといけないのか?それがあるがゆえに、'その変数はそこには許されない、なぜなら、それは'const'を含んでいるから'のような一部の制約に煩わされないといけないことになる。. . .これは、マゾヒズムなのか?
違う。私が'const'を使うのは、それが、コードを明確化するのにとても有益だからだ。
私がコードを読むとき、私の関心事は通常、データたちがどこで変更されるかであり、'const'指定は、見なければいけない範囲を信頼できる形で縮小でき、それは、とても大きな助けである。
したがって、私のスタンスは、'const'は、使用できるすべての所で使うべきというものだ。
そして、ひと度、'const'を使うように決定すると、通常、それを中途半端には使えない、なぜなら、'const'の使用は連鎖するから: もしも、あるオブジェクトが'const'であると宣言されると、そのオブジェクトの、使用されるインスタンスメソッドたちは、'const'にしないといけないし、そのオブジェクトが渡されるリファレンス引数たちは'const'にしないといけない、等々。
3: 用いるべき記法
Hypothesizer 7
以下の記法はC++標準によって許されている。
@C++ ソースコード
const string * const l_string0 (new const string ("a string"));
えーと、1番目の「const」は「string」を左から修飾し、2番目の「const」は「const string *」を右から修飾している。
そのようなルールは、首尾一貫しているように思われない、あるときは左から修飾し、他のときは右から修飾するとは。
第2の「const」が'const const string *'のように左から修飾できない理由は理解できる: もしも、「const string」が'string'になったら、それは'const string *'になるだろうが、それは、'(const string) *'なのか'const (string *)'なのか曖昧だ。
実のところ、もっと良い記法がある。
@C++ ソースコード
string const * const l_string0 (new string const ("a string"));
その記法においては、'const'は常に右から修飾する: 第1の「const」は「string」を修飾し、第2の「const」は「string const *」を修飾し、したがって、'( (string const) *) const'となる。
その記法は、コンスタントメソッドは以下のように書かれるという事実とも整合している。
@C++ ソースコード
void method1 () const;
その「const」は右から修飾していますよね?
私は常に、より良い方の記法を用いる。
4: 様々な'const'たち
Hypothesizer 7
'const'は、それが使われている箇所の応じて異なる意味を帯び、ケース毎に分けて検討する必要がある。
準備として、1個のクラス、'ClassA'を作成する、以下のように。
theBiasPlanet/coreUtilitiesTests/constantsTest2/ClassA.hpp
@C++ ソースコード
#ifndef __theBiasPlanet_coreUtilitiesTests_constantsTest2_ClassA_hpp__
#define __theBiasPlanet_coreUtilitiesTests_constantsTest2_ClassA_hpp__
#include <iostream>
#include <string>
using namespace ::std;
namespace theBiasPlanet {
namespace coreUtilitiesTests {
namespace constantsTest2 {
class ClassA {
private:
string i_string {"a string"};
public:
string i_publicString1 {"a public string"};
string * i_publicString2 {new string ("another public string")};
ClassA ();
ClassA (string const & a_string);
virtual ~ClassA ();
ClassA (ClassA const & a_copiedObject);
virtual ClassA & operator = (ClassA const & a_assignedFromObject);
virtual void append (string const & a_string);
virtual string get () const;
virtual string & doesNotChangeObject ();
friend ostream & operator << (ostream & a_outputStream, ClassA const & a_classA);
};
}
}
}
#endif
theBiasPlanet/coreUtilitiesTests/constantsTest2/ClassA.cpp
@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/constantsTest2/ClassA.hpp"
namespace theBiasPlanet {
namespace coreUtilitiesTests {
namespace constantsTest2 {
ClassA::ClassA () {
}
ClassA::ClassA (string const & a_string): i_string (a_string) {
}
ClassA::~ClassA () {
if (i_publicString2 != nullptr) {
delete i_publicString2;
i_publicString2 = nullptr;
}
}
ClassA::ClassA (ClassA const & a_copiedObject): i_string (a_copiedObject.i_string) {
}
ClassA & ClassA::operator = (ClassA const & a_assignedFromObject) {
i_string = a_assignedFromObject.i_string;
return *this;
}
void ClassA::append (string const & a_string) {
i_string.append (a_string);
}
string ClassA::get () const {
return i_string;
}
string & ClassA::doesNotChangeObject () {
return i_string;
}
ostream & operator << (ostream & a_outputStream, ClassA const & a_classA) {
a_outputStream << a_classA.i_string;
return a_outputStream;
}
}
}
}
4-1: 任意のノーマル変数に作用するとき
Hypothesizer 7
'const'は任意のノーマル変数に対して使うことができる、以下のように。
@C++ ソースコード
ClassA const l_classA {string ("a string")};
それが意味することは、1) その変数の値は変更できない、および 2) そのオブジェクトの非'const'インスタンスメソッドはどれもコールできない。
念のために言うと、1)は、その変数には再代入ができないこと、および、その変数内のオブジェクトを変更できないことの両方を意味する。
@C++ ソースコード
l_classA = ClassA (string ("another string")); // not allowed: reassigning
l_classA.i_publicString1.append (string (" P.S. Do not forget.")); // not allowed: changing the object
勿論、以下は許される、なぜなら、'l_publicString2'によってポイントされている文字列オブジェクトは'l_classA'オブジェクトの1部ではないから(その文字列オブジェクトのアドレスはそうであるが)。
@C++ ソースコード
l_classA.i_publicString2->append (string (" P.S. Do not forget."));
これも念のためだが、2)は、1)に含まれていない、なぜなら、当該オブジェクトを変更しないが'const'であると宣言されていないどのインスタンスメソッドもコールできないから。
@C++ ソースコード
l_classA.doesNotChangeObject (); // not allowed: calling a non-'const' method
l_classA.get ();
4-2: On Any Pointer 任意のポインターに作用するとき
Hypothesizer 7
'const'は、任意のポインターに対して使用できる、以下のように。
@C++ ソースコード
ClassA l_classA0 {string ("a string")};
ClassA l_classA1 {string ("another string")};
ClassA const * const l_classA2 {&l_classA0};
ClassA * const l_classA3 {&l_classA0};
ClassA const * l_classA4 {&l_classA0};
'ClassA'に係るそれらの「const」たちのそれぞれ(第3行の第1の「const」および第5行の第1の「const」)が意味するのは、その変数によってポイントされるデータは、その変数を介しては変更できないということだ。
「その変数によってポイントされているデータは'const'である。」や「その変数によってポイントされているデータは、その変数の生存期間を通して変更されないことが保証されている。」といった不正確な説明によって騙されないことが重要だ。
以下を見てみよう(それは、完全に正当である)。
@C++ ソースコード
cout << "### l_classA2 before changed: " << *l_classA2 << endl << flush;
l_classA0.append (string (" P.S. Do not forget."));
cout << "### l_classA2 after changed: " << *l_classA2 << endl << flush;
@出力
### l_classA2 before changed: a string
### l_classA2 after changed: a string P.S. Do not forget.
その変数によってポイントされるデータは、'const'でなく、いつでも変わりうる。
禁止されているのは、以下のようなものだけである。
@C++ ソースコード
l_classA2->i_publicString1.append (string (" P.S. Do not forget.")); // not allowed: changing the object via the variable
推測できる通り、以下は許される。
@C++ ソースコード
l_classA2->i_publicString2->append (string (" P.S. Do not forget."));
「ClassA const *」または「ClassA *」に係るその「const」(第3行の第2の「const」および第4行の第1の「const」)が意味するのは、そのポインターには再代入できないということだ。
したがって、以下は禁止される。
@C++ ソースコード
l_classA2 = &l_classA1; // not allowed: reassigning
l_classA3 = &l_classA1; // not allowed: reassigning
4-3: 任意のリファレンスに作用するとき
Hypothesizer 7
'const'は、任意のリファレンスに対して使用できる、以下のように。
@C++ ソースコード
ClassA l_classA0 {string ("a string")};
ClassA const & l_classA1 {l_classA0};
それが意味するのは、その変数によって参照されているそのデータは、その変数.を介しては変更できないということだ。
ここでも、「その変数によって参照されているデータは'const'である。」や「その変数により参照されているデータは、その変数の生存期間中変わらないことが保証されている。」といった不正確な説明によって騙されないことが重要だ。
以下を見てみよう(それは、完全に正当である)。
@C++ ソースコード
cout << "### l_classA1 before changed: " << l_classA1 << endl << flush;
l_classA0.append (string (" P.S. Do not forget."));
cout << "### l_classA1 after changed: " << l_classA1 << endl << flush;
@出力
### l_classA1 before changed: a string
### l_classA1 after changed: a string P.S. Do not forget.
その変数によって参照されているデータは、'const'ではなく、いつでも変わりうる。
禁止されているのは以下のようなものだけである。
@C++ ソースコード
l_classA1.i_publicString1.append (string (" P.S. Do not forget.")); // not allowed: changing the object via the variable
推測できる通り、以下は許される。
@C++ ソースコード
l_classA1.i_publicString2->append (string (" P.S. Do not forget."));
'ClassA & const'のような構文は存在しない、なぜなら、リファレンスは、どのみち再代入できないから。
4-4: 任意の配列に作用するとき
Hypothesizer 7
'const'は、任意の配列に対して使用できる、以下のように。
@C++ ソースコード
ClassA l_classA {string ("a string")};
ClassA const l_array1 [1] {string ("another string")};
ClassA const * const l_array2 [1] {&l_classA};
ClassA * const l_array3 [1] {&l_classA};
ClassA const * l_array4 [1] {&l_classA};
それらの「const」は各要素に関するものであり、要素に対する効果は、上記記述のそれぞれ対応する部分に一致する、その要素がノーマル変数であるかポインターであるかに依って(リファレンスではありえない)。
なぜ、要素はリファレンスではありえないのか?その理由は、配列というものはメモリのひと続きのエリアを意味するが、そのようなリファレンスは、その配列エリアの外に存在しているある既存のデータの位置を意味することになってしまうだろう(そのことは、この記事が明らかにするだろう)。
しかし、任意の配列全体へのリファレンスは定義できる、以下のように。
@C++ ソースコード
ClassA l_array0 [1] {string ("a string")};
ClassA const (& l_array6) [1] {l_array0};
以下は許されない。
@C++ ソースコード
l_array6 [0].i_publicString1.append (string (" P.S. Do not forget.")); // not allowed: changing the element object via the variable
同様に、任意の配列全体へのポインターは定義できる、以下のように。
@C++ ソースコード
ClassA const (* const l_array5) [1] {&l_array0};
第2の「const」は、配列ポインターに関するものであり、その効果は、任意のポインターに対する効果と何も変わらない。
'ClassA l_array0 [1] const'のような構文は存在しない、なぜなら、配列には、どのみち再代入などできないから。
4-5: 典型的なコンテナに作用するとき
Hypothesizer 7
任意のコンテナに作用する'const'は、特に、本当に特別というわけではないが、'::std::map'という典型的なコンテナに対するいくつかのケースを見てみよう、それらはいくぶんか混乱を誘うところがあるかもしれないから。
'const'は、'map'に対して使用できる、以下のようにして(ノーマル変数についてのみ考えよう、なぜなら、他のケースにおける効果は、この考慮から容易に推測できるだろうから)。
@C++ ソースコード
map <string const, ClassA const> const l_map00 { {"a key string", string ("a value string")}};
map <string, ClassA const> const l_map0 { {"a key string", string ("a value string")}};
map <string, ClassA> const l_map1 { {"a key string", string ("a value string")}};
map <string, ClassA const> l_map2 { {"a key string", string ("a value string")}};
えーと、そのキータイプに対する「const」は許される(少なくともGCC 9.3.0では)、しかし、それは有意義だろうか?
実のところ、キーはどのみちリードオンリーだ: 以下は許されない。
@C++ ソースコード
l_map0.find (string ("a key string"))->first = string ("another key string"); // not allowed anyway: reassigning the key
l_map0.find (string ("a key string"))->first.append (string (" P.S. Do not forget.")); // not allowed anyway: changing the key object
私は、キーに意義なく'const'をつけるのを控えることにする。
'map <string, ClassA> const'と'map <string, ClassA const>'は、後者はその変数の再代入を許すという点において異なる。
@C++ ソースコード
l_map1 = map <string, ClassA> { {"another key string", string ("another value string")}}; // not allowed: reassigning
l_map2 = map <string, ClassA const> { {"another key string", string ("another value string")}};
'map <string, ClassA const> const'は、'map <string, ClassA> const'と比較して有意義には思われない、なぜなら、'ClassA'値はどのみち変更できないから。
@C++ ソースコード
l_map1.at (string ("a key string")) = ClassA (string ("another value string")); // not allowed: reassigning the value
(l_map1.at (string ("a key string"))).i_publicString1.append (string (" P.S. Do not forget.")); // not allowed: changing the value
その「ClassA const」という部分は、'ClassA const * const'、'ClassA * const'、'ClassA const *'、'ClassA const &'になりうるが、それらの意味は、上記サブセクション群のいくつかにて説明されたものを何も違わない。
4-6: 任意のファンクションリターンに作用するとき
Hypothesizer 7
'const'は、任意のファンクションリターンに対して使用できる、以下のように。
@C++ ソースコード
virtual ClassA const method2 ();
virtual ClassA const * const method3 ();
virtual ClassA * const method4 ();
virtual ClassA const * method5 ();
virtual ClassA const & method6 ();
それらのそれぞれは、任意のファンクションコール表現は上記サブセクション群で説明された制約を帯びることを意味する。
以下の例は、私の意味することを明確にするだろう。
@C++ ソースコード
ClassB l_classB;
l_classB.method2 ().i_publicString1.append (string (" P.S. Do not forget.")); // not allowed: changing the object
l_classB.method3 ()->i_publicString1.append (string (" P.S. Do not forget.")); // not allowed: changing the object via the function call expression
l_classB.method4 ()->i_publicString1.append (string (" P.S. Do not forget."));
l_classB.method5 ()->i_publicString1.append (string (" P.S. Do not forget.")); // not allowed: changing the object via the function call expression
l_classB.method6 ().i_publicString1.append (string (" P.S. Do not forget.")); // not allowed: changing the object via the function call expression
「l_classB.method2 ()」は、ファンクションコール表現だ、例えば。
以下は問題ない。
@C++ ソースコード
ClassA l_classA = l_classB.method2 ();
l_classA.i_publicString1.append (string (" P.S. Do not forget."));
その理由は、「l_classA」は、リターンされた'const'オブジェクトのコピー(それは'const'でない)を格納することだ、リターンされた'const'オブジェクトそのものをではなくて。
4-7: 任意のクラスインスタンスメソッドに作用するとき
Hypothesizer 7
'const'は、任意のクラスインスタンスメソッドに対して使用することができる、以下のように。
@C++ ソースコード
void method1 () const;
ある以前の記事にて詳細に説明された通り、任意のクラスインスタンスメソッドに対する'const'の意味は、そのメソッドは'const'オブジェクトに対してもコールできるということであって、「そのメソッドは当該オブジェクトを変更しない」ということではない。
例えば、以下は許されない、そのメソッドは当該オブジェクトを全く変更しないにもかかわらず。
theBiasPlanet/coreUtilitiesTests/constantsTest2/ClassA.hpp
@C++ ソースコード
#ifndef __theBiasPlanet_coreUtilitiesTests_constantsTest2_ClassA_hpp__
#define __theBiasPlanet_coreUtilitiesTests_constantsTest2_ClassA_hpp__
#include <iostream>
#include <string>
using namespace ::std;
namespace theBiasPlanet {
namespace coreUtilitiesTests {
namespace constantsTest2 {
class ClassA {
private:
string i_string {"a string"};
public:
~
virtual string & doesNotChangeObject () const;
~
};
}
}
}
#endif
theBiasPlanet/coreUtilitiesTests/constantsTest2/ClassA.cpp
@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/constantsTest2/ClassA.hpp"
namespace theBiasPlanet {
namespace coreUtilitiesTests {
namespace constantsTest2 {
~
string & ClassA::doesNotChangeObject () const {
return i_string;
}
~
}
}
}
任意の'const'メソッドの中で、'this'(それはキーワードであって、ポインターではない)は'const'になる(なぜ、それが上記コードを禁止するかは次セクションにて明確にされる)。
5: 様々な'const'に対処する
Hypothesizer 7
様々な'const'の意味を今や理解したので、様々な'const'に対処する方法を見てみよう。
例えば、ある変数は、'const'性を持つどのようなファンクション引数群に渡すことができるのか?または、ある変数は、'const'性を持つどのようなファンクションリターンとしてリターンできるのか?
5-1: 用語体系
Hypothesizer 7
私の用語体系においては、任意のそしてただ、値が変更できない変数だけが、'const'変数と呼ばれる。
そんなの当然?えーと、私が言いたいのは、以下は'const'変数ではないということだ。
@C++ ソースコード
ClassA const * l_classA {new ClassA (string ("a string"))};
なぜか?なぜなら、'l_classA'の値(それは、アドレス以外の何物でもない)は変更できる。その「const」は、その変数の値に関するものではない。
したがって、なんらかの'const'指定を持つ全ての変数が'const'変数というわけではない。 .
なんらかの'const'指定を持つが'const'変数ではない変数のことを、'const'関連変数、と私は呼ぶことにする。
5-2: 'const'変数を初期化する
Hypothesizer 7
任意の'const'変数は、基本、定義時に初期化されなければならないが、もしもそれがクラスインスタンスフィールドであればコンストラクタのイニシャライザーによって初期化されることができ、もしもそれがファンクション引数であればファンクションコールによって初期化されることができる。
以下は、コンストラクタのイニシャライザーによって初期化を行なう例だ。
theBiasPlanet/coreUtilitiesTests/constantsTest2/ClassA.hpp
@C++ ソースコード
#ifndef __theBiasPlanet_coreUtilitiesTests_constantsTest2_ClassC_hpp__
#define __theBiasPlanet_coreUtilitiesTests_constantsTest2_ClassC_hpp__
#include <string>
using namespace ::std;
namespace theBiasPlanet {
namespace coreUtilitiesTests {
namespace constantsTest2 {
class ClassC {
private:
string const i_string;
public:
ClassC (string const & a_string);
};
}
}
}
#endif
theBiasPlanet/coreUtilitiesTests/constantsTest2/ClassC.cpp
@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/constantsTest2/ClassC.hpp"
namespace theBiasPlanet {
namespace coreUtilitiesTests {
namespace constantsTest2 {
ClassC::ClassC (string const & a_string): i_string (a_string) {
}
}
}
}
ファンクション引数に何かを渡すことは、その引数を初期化することになるという点に注意する。
5-3: どの変数に何がセットできるか
Hypothesizer 7
支配している原則を理解しよう。
第1に、各'const'によって修飾されているものがコピーされるものなのか否かを見分けなければならない。
以下は、コピーされるものを修飾している'const'の例だ。
@C++ ソースコード
ClassA const l_classAFrom0 {string ("a string")};
ClassA * const l_classAFrom1 {new ClassA (string ("a string"))};
ClassA const & l_classAFrom2 {* (new ClassA (string ("a string")))};
ClassA l_classATo0 = l_classAFrom0;
ClassA * l_classATo1 = l_classAFrom1;
ClassA l_classATo2 = l_classAFrom2;
'l_classAFrom1'についての'const'はアドレスに対するものであって、アドレスはコピーされているでしょう?
その一方、以下は、コピーされないものを修飾している'const'の例だ。
@C++ ソースコード
ClassA const * l_classAFrom3 {new ClassA (string ("a string"))};
ClassA * l_classATo3 = l_classAFrom3; // not allowed
その'ClassA'データはコピーされていないでしょう?
第1の原則は、コピーされるものを修飾する任意の'const'は、それがセットされることにも、それにセットされることにも、何の制約もかけないということだ。
なぜなら、オリジナルは、そのコピーが生成されても何の害も受けえないから、そのコピーが、新たに'const'になろうとなるまいと: それらは、単に2つの別オブジェクトにすぎない。
コピーされないものを'const'が修飾する時にのみ、何らかの安全性の考慮が働く。
第2の原則は、コピーされないものを'const'が修飾するとき、その'const'は、取り除けないが、付け加えることはできるということだ。
例えば、以下は許されない。
@C++ ソースコード
ClassA const * l_classAFrom4 {new ClassA (string ("a string"))};
ClassA const & l_classAFrom5 {* (new ClassA (string ("a string")))};
ClassA * l_classATo4 = l_classAFrom4; // not allowed: 'const' is stripped
ClassA & l_classATo5 = l_classAFrom5; // not allowed: 'const' is stripped
えーと、取り除くことの禁止は、決して論理的必然ではない(そのタイプの'const'は、当該データはその変数を介して変更できないことのみを意味し、そのデータを新たな非'const'変数を介して変更することは、必ずしも悪いことではない)、しかし、デフォルトの振る舞いとしては、それは実際的である(もしも、'const'性がそんなに無造作に取り除けてしまったとしたら、'const'は、その実際的有用性を失ってしまうだろう)。
しかし、以下は許される。
@C++ ソースコード
ClassA * l_classAFrom6 {new ClassA (string ("a string"))};
ClassA & l_classAFrom7 {* (new ClassA (string ("a string")))};
ClassA const * l_classATo6 = l_classAFrom6; // 'const' is augmented
ClassA const & l_classATo7 = l_classAFrom7; // 'const' is augmented
付け加えることが許されるのは、それは安全性を増すだけであり、C++標準は、増された安全性に異議を挟まないから。
以上が、原則の全てだ、狭く言えば。
「狭く言えば」と言ったわけは、オブジェクト指向プログラミング言語のある原則もそこで支配しているから。
例えば、以下は許されない。
@C++ ソースコード
map <string, ClassA> l_mapFrom0 { {"a key string", string ("a value string")}};
map <string, ClassA *> l_mapFrom1 { {"a key string", new ClassA (string ("a value string"))}};
map <string, ClassA const> * l_mapTo0 = &l_mapFrom0; // not allowed: incompatible types
map <string, ClassA const *> * l_mapTo1 = &l_mapFrom1; // not allowed: incompatible types
なぜか?それらの'const'は、付け加えられたものであり、それは問題でないはずでしょう?. . .えーと、それらの禁止は、'const'性の安全性考慮からきたものではなく、オブジェクト指向プログラミング言語のある原則からきたものだ: 任意のポインターまたはリファレンスによってポイントまたは参照されるオブジェクトは、そのポインターまたはリファレンスのタイプの'is-a'関係になければならない。
ここでの要点は、「ClassA const」および「ClassA const *」における「const」は、データタイプの一部であると見なされるということだ。
. . .それはリーズナブルだろうか?えーと、「ClassA const」について言えば、それは理解できる、なぜなら、その「const」はそのデータ自体の属性だから(そのデータは間違いなく'const'だ)、しかし、「ClassA const *」について言えば、そうは思わない: その「const」は本当にそのデータの属性であるわけではない、なぜなら、そのデータは必ずしも'const'ではないから。
それはともかく、「map <string, ClassA>」および「map <string, ClassA *>」が、それぞれ、「map <string, ClassA const>」および「map <string, ClassA const *>」の'is-a'関係にはないと結論するロジックは、Java配列についてのある記事におけるロジックと同一だ、もしも、それらの「const」がそれらデータ タイプの一部であるとみなされるのであれば。
当然、任意のテンプレートや配列についても、状況は同様だ。
5-4: どのリターンタイプとして何がリターンできるか
Hypothesizer 7
ファンクションリターンタイプとリターンされるものの間の関係は、セット先変数タイプとセットされるものの間のそれ(前サブセクションで説明された)と同じだ。
例えば、以下は全く問題ない、「const」がコピーされるものに係っているから。
@C++ ソースコード
ClassA ClassB::method7 () {
ClassA const l_classA {string ("a string")};
return l_classA;
}
ClassA * ClassB::method8 () {
ClassA * const l_classA {new ClassA (string ("a string"))};
return l_classA;
}
ClassA ClassB::method9 () {
ClassA const & l_classA {* (new ClassA (string ("a string")))};
return l_classA;
}
以下は許されない、「const」がコピーされないものに係っていて、「const」が取り除かれているから。
@C++ ソースコード
ClassA * ClassB::method10 () {
ClassA const * l_classA {new ClassA (string ("a string"))};
return l_classA; // not allowed: 'const' is stripped
}
以下は許される、「const」がコピーされないものに係っているが、「const」は付け加えられているから。
@C++ ソースコード
ClassA const * ClassB::method5 () {
ClassA * l_classA {new ClassA ()};
return l_classA; // 'const' is augmented
}
ClassA const & ClassB::method6 () {
ClassA * l_classA {new ClassA ()};
return *l_classA; // 'const' is augmented
}
以下は許されない、リターンされているものが、リターンタイプの'is-a'関係にないから。
@C++ ソースコード
map <string, ClassA const> * ClassB::method11 () {
map <string, ClassA> l_map { {"a key string", string ("a value string")}};
return &l_map; // not allowed: incompatible types
}
map <string, ClassA const *> * ClassB::method12 () {
map <string, ClassA *> l_map { {"a key string", new ClassA (string ("a value string"))}};
return &l_map; // not allowed: incompatible types
}
5-5: C++標準がアンリーズナブルな時
Hypothesizer 7
ある前のサブセクションにて、C++標準のリーズナブル性について疑問を挙げた。
ここで、別の疑問を挙げよう。
1つの例を考えてみよう。
@C++ ソースコード
ClassA * ClassB::method10 () {
ClassA const * l_classA {new ClassA (string ("a string"))};
return l_classA; // not allowed: 'const' is stripped
}
それは許されないが、その理由は、'ClassA'に係る「const」が、リターン時に取り除かれていることだ。
しかし、それについて私はハッピーではない: その「const」が意味することは、そのデータはその変数を介しては変更できないということだけであって、そのデータがそのファンクションの外でコンスタントであるべきだなどと、私は全然、意図していない。. . .そのファンクション内でのみ有効であるその変数を介して何をできないかについてだけであるその「const」性を、そのファンクションが終了した後についてのものであるそのリターンタイプに強制するのは、私には意味を成さない。
全体的に、C++標準は'const'の様々な意味の区別においてずさんであると、言わざるをえない。
ところで、C++が私の癪に障る点の1つに、以下が許されないことがある。
@C++ ソースコード
void ClassB::method13 (ClassA & a_classA) {
a_classA.append (string (" P.S. Do not forget. Do not forget."));
cout << "### a_classA: " << a_classA << endl << flush;
}
~
ClassB l_classB;
l_classB.method13 (ClassA {string ("a string")}); // not allowed: a rexpression is passed into
~
任意のrexpression(実態とかけ離れた名前で一般に「rvalue」と呼ばれている)はその引数に渡すことができないが、そのような制約に対する納得のいくような理由を私は1つも聞いたことがない: 渡されたそのデータはそのファンクションが終了するまで生存しているのであり、そのファンクションの内部でそれが変更されることに何の問題もない。
以下のようにするように強いられてしまう(うっとうしい!)、私は'ClassA'オブジェクトをテンポラリーにしかしたくないのに。
@C++ ソースコード
ClassA l_classA {string ("a string")};
l_classB.method13 (l_classA);
もしくは、以下のように、実態とかけ離れて一般に「ムーブセマンティクス」と呼ばれているものを使って(これは、「ムーブ」とは本当に何の関係もない!)。
@C++ ソースコード
void ClassB::method14 (ClassA && a_classA) {
a_classA.append (string (" P.S. Do not forget. Do not forget."));
cout << "### a_classA: " << a_classA << endl << flush;
}
'ClassA const & a_classA'は私の選択肢にはないことを言っておく、なぜなら、'a_classA'は、そのメソッド内で変更されなければならないから。
C++標準が時としてアンリーズナブルであることを私たちは認める必要があり、その認識が次サブセクションへとつながる。
5-6: 'const_cast'を使用して'const'性を取り除く
Hypothesizer 7
'const'性は、'const_cast'を使用して取り除くことができる、以下のようにして。
@C++ ソースコード
ClassA * ClassB::method16 () {
ClassA const * l_classA {new ClassA (string ("a string"))};
return const_cast <ClassA *> (l_classA); // I use 'const_cast' just because the C++ standard is unreasonable.
}
. . .'const'性を取り除くのは推奨できないことではないのか?えーと、'const_cast'を使うことを強固に否定する一部の人々がいるかもしれないが、私が考えるには、それは完全にオーケーだ、もしも、私はC++標準のアンリーズナブル性を相殺しているだけなのであれば。私はそうするよう追い詰められたのだ、C++標準がずさんであるがゆえに!. . .勿論、自分が何をしているかを私は知っていなければならないが、C++は、いつだって、自分が何をしているかを知っていなければならないプログラミング言語以外の何物でもなかったのだ。
不運なことに、サブセクション、'5-3'にて挙げたアンリーズナブル性に対抗する方法を私は知らない('const_cast'は、それを是正するのには使えない)。