C/C++ の const 修飾子の位置で混乱しないために
C や C++ の const 修飾子は変数や引数に指定することで “値が不変である” ということを示す. 極めて単純である.
これは書き方にいくつかのバリエーションが存在するが、ポインタ変数に対して指定する場合には初見だと非常に混乱する記述となる.
この記事ではこの const 修飾子を混乱せずに使うための考え方についてまとめる.
変数に対して const 指定する場合
int a = 10; のように宣言された変数を const 指定する場合には、以下のいずれかの書き方がある.
const int a = 10; // 変数 a の値は書き換えできなくなる.
int const a = 10; // 同上
このいずれかの記述を行った場合、変数 a は値を書き換えることができなくなる.
すなわち、a = 20; のような代入を行おうとするとコンパイルエラーが発生するようになる.
これらは 2 通りの書き方で同じ効果が得られるので “どちらの書き方でも良い” とよく説明される.
つまり、const int と int const はどちらも “不変 (constant) な int 型” を表すのである.
よって const int a や int const a は “不変な int 型の変数 a” と読むわけである.
ポインタ変数に対して const 指定する場合
さて、問題のポインタ変数に対して const 指定をする場合についてである.
int a = 10; として宣言されている変数を int *p = &a; のようにポインタ変数 p で参照するとしよう.
このポインタ変数 p に対して const 指定をする場合、const を付与する位置に応じて異なる効果が現れることになる.
const int *p = &a; // ポインタ変数 p を介して a の値を書き換えできなくなる. (ただし、p の参照先を変更することは可能.)
int const *p = &a; // 同上
int* const p = &a; // ポインタ変数 p の参照先を変更できなくなる. (ただし、p を介して a の値を書き換えることは可能.)
もう見るからにややこしい.
はじめの 2 つ (const int *p = &a;* と int const *p = &a;) は、p を介した a の書き換えを禁止する.
すなわち、*p = 20; などとして a の値を 10 から 20 に変更するような書換はできなくなる.
しかし、int b = 100; のように別の変数が定義されているとして p = &b; のように途中で p の参照先を変更することは
禁止されておらず、自由に行える.
また、逆に 3 つ目の int* const p = &a; は p の参照先を変更できなくするため、p = &b; のような代入はできなくなる.
しかし、*p = 20; のように p を介して a の値を書き換えるような処理は問題なく行える.
p を介した a の書き換えと p の参照先の変更をどちらも禁止したい場合には、以下のどちらかを記述すれば良い.
const int* const p = &a;
int const* const p = &a;
これにより、*p = 20; といった処理も p = &b; といった処理もどちらも禁止することができる.
さて、問題となるのは明らかに これらの const を付与する位置による差異が紛らわしすぎる という事実である.
もう一度まとめてみよう.
int * p = &a; // 通常のポインタ変数
const int * p = &a; // p を介した a の書き換えを禁止.
int const * p = &a; // 同上
int * const p = &a; // p の参照先の変更を禁止.
const int * const p = &a; // p を介した a の書き換えと p の参照先の変更をどちらも禁止.
int const * const p = &a; // 同上
さて、このままでは明らかに覚えにくい. どのように覚えればよいだろう.
まず、最初に見た「変数に対して const 指定する場合」と同様に、全く同じ効果を持つ 2 番目と 3 番目が const int と int const の違いしかないことに着目する.
これは const int と int const がどちらも “不変 (constant) な int 型” を示すということを思い出せば納得できるだろう.
これは 5 番目と 6 番目も同様である.
これを踏まえてもう一度グループに分けてみると以下のようになる.
int * p = &a; // 通常のポインタ変数
const int * p = &a; // p を介した a の書き換えを禁止.
int const * p = &a;
int * const p = &a; // p の参照先の変更を禁止.
const int * const p = &a; // p を介した a の書き換えと p の参照先の変更をどちらも禁止.
int const * const p = &a;
すると、* の右側と左側のどちらに const が現れるのかによって効果が変化しているという事実に気づく.
これは以下のような図として表現できるだろう.
int * p = &a;
,- int -. *
| a | <----- p
'-------'
const int * p = &a;int const * p = &a;
,- int -. *
| a | <----- p
'-------'
const
int * const p = &a;
,- int -. *
| a | <----- p
'-------' const
const int * const p = &a;int const * const p = &a;
,- int -. *
| a | <----- p
'-------' const
const
要するに、* を参照の矢印と見立てて * の左側に参照先、右側にポインタ変数があると仮定し、
* の左側に const が現れていれば “参照先が不変 = 参照先の書き換えができない”、
* の右側に const が現れていれば “ポインタ変数そのものが不変 = 参照先を変更できない” と考えるわけである.
すると、* の両側に const が現れていればどちらも不変となるのも納得しやすいだろう.
だいぶ大雑把な覚え方だが、このように見做せばポインタ変数の const 修飾子で迷わずに済むのではないだろうか.
const の挙動をもう少し正確に
さて、* の左側と右側のどちらに現れるのかによって const の挙動が違うというのはなかなか分かりづらい仕様であると思う.
なぜこのように書くのだろうか.
C++ の例となってしまうが、 こちら の記事中では
const、volatile、*、&、&& といったものは全て “既存の型に後ろから修飾することで新しい型を作ることができる” ものであると説明されている. (C++ において & および && は参照型の変数に対して付与される. C 言語には存在しない.)
また、const や volatile は既存の型の直後に修飾できる状況に限って、既存の型の前に修飾することで新しい型を作れるという.
つまり、const のようなキーワードは普通は int などの型に後ろから修飾することで int const (不変な int 型) のように “新たな型” を作るということである.
また、このように直後から修飾できる文脈に限って const int のように前から修飾することもできる.
面白いのはポインタ変数を宣言する際に使用する * もこのように “後置によって新たな型を作るキーワードである” ということだ.
すなわち、int 型へのポインタ変数を int *p; のようにして宣言することが多いが、これは意味的には実際は int* p; であるということだ.
(int 型に対するポインタ型 int* の変数 p の宣言.)
では何故 int* p ではなく int *p のように書く場合が多いのだろうか.
これは、C や C++ では 2 つ以上の同じ型の変数をカンマで区切ることで int a, b; のように宣言することができるが、2 つのポインタ変数を宣言するつもりで int* p, q; のように宣言しても p が int 型へのポインタ変数として宣言される一方で q は通常の int 型変数として宣言されてしまうという仕様が存在するため であると考えられる.
すなわち、複数のポインタ変数を宣言する場合には毎回 “変数名の直前に * を付与する” というルールが浸透しているためであろう.
これは * が既存の型名に後ろから修飾することで新たな型を作るキーワードであるという観点からは明らかに相容れない仕様であり、C や C++ の仕様の欠陥であるように思われる.
いずれにしても、このようにして “後置することで既存の型から新たな型を作る” という観点で const や * を眺めてみると新たな発見がある.
例えば、int 型などの既存の型を仮に A 型と書くことにすれば、A 型へのポインタ型は A* と書くことによって作ることができる. ではこれを “新たな既存の型” として B 型と書くことにしよう.
さて B 型は既に “既存の型” であることから、B 型へのポインタ型を * を後置することで作ることもできるだろう. すなわち、B* である.
ここで、B 型が元々どんな型であっただろうと思い返すと、B* 型は以下のように展開することができることがわかる.
(ここではわかりやすくするために便宜上カッコ ( ) を使用して型を括っているが、もちろんカッコを使って型を書こうとしても C や C++ では型としては認識されないのでコンパイルエラーである.)
B*
=== (A*)*
=== A**
ということで、このようにして A 型へのダブルポインタ (A 型へのポインタのポインタ) 型が得られる.
これは面白い. B に対して * の代わりに const を後置すれば B const 型、すなわち A* const 型を得ることができる.
B const
=== (A*) const
=== A* const
これは * の右側に const が現れるパターンであり、これは “ポインタ変数の参照先を変更することができない = ポインタ変数に対する再代入ができない” という意味であった.
この意味は、const が A* に対して後置することで修飾しているという事実を考えれば明らかである.
すなわち、A 型へのポインタ型 A* を const 修飾することで、“不変な (A 型へのポインタ) 型” を作ったことになるので、“不変” になるのは参照先ではなく、ポインタ変数そのものであるということだ.
ここで A が int const 型であるとすれば int const * const 型となり、先程考えていた “参照先の書き換えも、参照先の変更もできないポインタ型” となる.
また、const が既存の型 int に後置できている場合には、前置することで同じ効果を得ることもできるので const int を A 型として const int * const 型を作ってもやはり全く同様の型を作ることができる.
このようにして、* や const は “後置することで新たな型を作る. const を前置するのはちょっと特殊な場合” という事実を理解すれば、ポインタ型における const をつける位置について理解することも簡単にできるのではないかと思う.