こんにちは。田原です。
書き換えてはいけない変数を間違って書き換えるバグのデバッグはたいへんです。問題発生時のログを精査するなどいつも苦労します。しかし、そんなこともあろうかとC++にはconstと言う概念が用意されています。constを付けた変数を書き換えようとするとコンパイル・エラーにすることでバグを事前に叩き潰すという仕組みです。constに一手間かけることで苦労を大きく削減する頼もしい仕組みなのです。
1.constの考え方
考え方は冒頭にも書いたように単純です。
「この変数は書き換えないよ。もし、書き換えていたら教えてね。」とコンパイラにお願いしているのです。そして、コンパイラは書き換えない筈の変数を書き換えるかもしれないコードを検出したらコンパイル・エラーにします。
実は、書き換えた時だけエラーにしてくれると「神」なのですが「書き換える可能性のあるコード」をエラーにします。そのため、慣れるまではconstをつけるべき場所を把握しづらく、コンパイルになかなか通らなくて作業が先に進まないので、ついイライラしてしまいます。(私自身、通ってきた道です。一時期はconstを使うのやめたことも...)
ですが、書き換えるべきでない変数を間違って書き換えているバグを発見するのは結構たいへんです。ログをあちこちに仕込んで問題が発生した時にログを精査し、何を間違っているのか探します。結果、想定外のところで変数の値を書き換えているとがっかりです。
その関数を書いた人が書き換えてはいけないことを把握していないため、そのような事が起きます。しかし、膨大な変数群の1つ1つに対して、書き換えて良い場所と悪い場所を適切に記述したドキュメントを用意するのは現実的ではありません。また、仮に作ったとしても、それをきちんと把握してそれを守れるのか?というと、かなり厳しいです。
そのような単純作業はコンピュータに任せるべきです。それがconstなのです。
2.constの書き方
C++のconstはまず「型」を修飾し、そのconst修飾された「型」の「変数」を定義します。それにより、変数の振る舞いを決めています。
2-1.const修飾の基本
const修飾された変数は初期化できますが、代入は禁止です。
const int ConstVar=123; // 初期化は可能 ConstVar=456; // これはコンパイル・エラー(代入不可)
2-2.ポインタ周りのconst修飾 First Level
これがいきなり難しいです。
ポインタを定義した時、2つの変数が絡んでいます。1つはポイント先の変数、もう一つはポインタ変数自身です。
int x=123; int* p=&x;
xがポイント先の変数で、pがポインタ変数自身です。
このpをconst修飾する時、constである(書き換えない)と宣言する対象が2つあります。1つはポイント先の変数、もう一つはポインタ変数自身ですね。
ポイント先の変数をconst修飾するには、int型変数を変更不可と宣言するので、intの直ぐ隣にconstと書きます。
int const* p=&x; const int* q=&x; // ①
この両者は「全く」同じ意味です。
ポインタ変数自身をconst修飾するには、int*
を変更不可と宣言します。int*
の直ぐ隣にconstを書きますが、前に書いてしまうと上記の①と区別ができませんので、後ろに書きます。
int* const r=&x;
そして、ポイント先の変数をconst修飾した場合、p経由でxを変更するとエラーになります。(直接xを変更してもエラーになりません。)
int x=123; int const* p=&x; x=456; // これはコンパイル・エラーにならない。 //*p=789; // これはコンパイル・エラーになる。
x自身はconstではないので変更できます。しかし、*p
はconstなint型ですので変更できないのです。
なお、ポインタ変数自身をconst修飾した場合は、そのポインタを変更できません。ポインタを指す先をx以外へ切り替えることができないと言う意味です。
int const* p; と const int *p; どちらの書き方がよい?
2つ書き方があると、どちらにしようか悩みますよね。
これについては const int *p; と書く人やコンパイラ(エラー・メッセージ)が多いです。
しかし、私は int const* p; と書いた方が分かりやすいと思います。慣れない内は特にです。
constを先に書いた場合、constが修飾するのがintなのか、int* なのか分かり難いと思います。そのため、どっちだっけ?と無駄に悩みます。少なくとも慣れない内は int const* p; と書くことをお勧めしますし、新人さんが入ってくるような環境でしたら同様に int const* p; と書いた方が新人さんの成長が僅かでも早くなるかも知れません。
2-3.ポインタ周りのconst修飾 Second Level
これは更に難しいです。ここが「書き換えない筈の変数を書き換えるかもしれないコードを検出」します。そのため、難しいのですがたいへんよく使われる重要な機能です。
int x=123; int const* p=&x; //int* q=p; // これはコンパイル・エラーになる。 int* q=&x; //*p=456; // これはコンパイル・エラーになる。 *q=456; // これはコンパイル・エラーにならない。
先に説明したように*p
はint const型変数ですから書き換えないと宣言してますので、*p=456;
などと書き換えようとするとエラーになります。しかし、*q=456;
はエラーになりません。*q
はconstが付いていないint型変数ですので。従って、もし、int* q=p;
を許すと*q=456;
できるのですから、一旦constであるpを経由したにも関わらず、簡単にpの指す先を変更できてしまいます。
この例のように並んでいれば間違うことはあまり無いと思いますが、次の例のようになっていると人はあっさり見落とします。
#include "baz.h" void bar(Foo const* iFoo) { baz(iFoo); // これはコンパイル・エラーですが、エラーにならかった場合を想定 } int main() { Foo aFoo(123); bar(&aFoo); }
struct Foo { int mData; Foo(int iData) : mData(iData) { } }; void baz(Foo* pFoo) { pFoo->mData=456; // 本当はコンパイル・エラーに成って欲しいがそれは無理 }
main関数からbarを呼び出したプログラマは、barの引数がFoo const*
で宣言されているため、aFooを渡しても中身を変更されないと期待します。・・・②
そして、baz(iFoo)と呼び出しました。bazの引数はFoo* pFoo
ですのでFoo const* iFoo
を代入するとエラーになります。しかし、もしも、これエラーにならなかった場合を考えます。
baz関数内でFooの内容を変更していますが、これは適切なコードですのでコンパイル・エラーにすることはできません。そこに変更不可なiFooを渡した結果、iFooの中身を変更できてしまいます。
②のプログラマにとって変更不可のはずなのに変更するとか「やめてくれっ」て話です。
そして、悲しいかな②と同じプログラマがbaz()のような関数を書くこともあります。baz()を書いた当初は変更していなかったとしても、暫くして修正する時にすっかりそのことを忘れて変更してしまうことは良くあることとと思います。
そこで、そのようなバグを生みにくいようにするため、上述のようにconstを指すポインタを非constを指すポインタへ代入するとエラーになるのです。
ちなみに、constな変数を指すポインタをconstでない変数を指すポインタへ代入することはできませんが、逆は問題なくできます。単に変更可な変数を変更できないと言うだけですから、当然問題ありませんし、上記bar関数のように特定の関数では変更させたくないような時に使います。
なかなか難しいので、JavaやC#はこのポインタの指す先がconstと言う概念の導入を断念してしまいました。そのため、クラス全体を変更出来ないようなイミュータブル・クラスなどを使って大雑把に指定するようです。それはそれで苦労しているようです。
2-4.ポインタ周りのconst修飾 あまり使わないLevel
他にも次のようなconst修飾ができます。
int x=123; int const*const p=&x;
pのポイント先のxを書き換えないし、p自身も書き換えないと言う宣言です。
稀に使いますが、あまり頻繁には使わないです。
更に次のようなconst修飾も可能です。
int x=123; int* p=&x; int const*const*const pp=&p;
まぁ、余興です。まず使うことは無いでょう。
2-5.参照周りのconst修飾 Third Level
ポイント先をconst修飾するのと同様に参照先をconst修飾する参照があります。
意味的にはポインタの場合と大差ないです。しかし、ポインタの場合よりも更に良く使います。
より多くのものを参照できるちょっと特殊な機能が同梱されているからです。
その分、更に難しかったりします。
まずは基本です。
int x=123; int const& r=x; int& s=x; x=456; // これはコンパイル・エラーにならない。 r=789; // これはコンパイル・エラーになる。 s=101112; // これはコンパイル・エラーにならない。
rはconst参照です。参照先のxをr経由で書き換えることができません。
sは通常の参照ですので、s経由でもxを書き換えることができます。
さて、ポインタ同様に次がちょっと難しいですが、考え方はポインタと同じです。
int x=123; int const& r=x; //int& s=r; // これはコンパイル・エラーになる。 int& s=x; //r=456; // これはコンパイル・エラーになる。 s=456; // これはコンパイル・エラーにならない。
rはint const型変数への参照ですから書き換えないと宣言してますので、r=456;
などと書き換えようとするとエラーになります。しかし、s=456;
はエラーになりません。s
は普通の参照ですので。しかし、もし、int& s=r;
を許すとs=456;
できるのですから、一旦constであるrを経由したにも関わらず、簡単にrの参照先を変更できてしまいます。これは2-3で説明したようにバグの元です。
ですので、非const型の参照をconst型の参照で初期設定できないことになっています。
2-6.参照周りのconst修飾のどうでも良い話
ポインタでは、int *const p;
と書けました。しかし、参照ではint &const r;
とは書けません。
ポインタ変数は変数ですから変更可能ですのでconstを付けて「これは書き換えません」と宣言する意味があります。
しかし、参照は変数ではありません。元々参照先を切り替えることができないものですからconstをつける意味がありません。なので、コンパイラが「何かの間違いでは?」と指摘してくれるのです。(実際何か間違っていることが多いと思います。本当はポインタを宣言したかったなど)
3.メンバ関数のconst修飾
C++では非staticなメンバ関数をconst修飾することができます。グローバル関数やstaticなメンバ関数は修飾できません。
非staticなメンバ関数は隠しパラメータとしてthisを受け取っています。
非staticなメンバ関数へのconst修飾は、そのthisの指す先(つまり自分)への修飾なのです。
ちょっと流れで解説してみます。
3-1.constではまりやすいところ
例えば、下記のようなクラスが有ったとします。
class Foo { int mData[1000]; public: Foo() : mData{} { } int get(size_t idx) { return mData[idx]; } };
これを次のように使ったとします。
void Bar(Foo const& iFoo) { }
Fooを参照で受け取ることでコピー負荷を節約しています。
単なる参照で受け取るとBarの中で渡されたFooの中身を変更できます。Barの中では受け取ったFooを変更しない場合も多いと思います。その時、上記のようにconstを付けてconst参照で受け取ることで「書き換えない」という宣言になるため、間違って書き換えるとエラーが出るのでバグを早期に検出できます。
しかし、このままでは1つ問題があります。
void Bar(Foo const& iFoo) { std::cout << iFoo.get(0) << "\n"; }
はコンパイルエラーになります。
get()は隠しパラメータとしてiFooへのポインタthisを持っています。隠しパラメータを隠さずに書いてみると次のようになります。(thisと記述するとコンパイル・エラーになるのでiThisとしました。)
第20回目で軽く触れているthisポインタへのconst修飾は下記のFoo*const iThisのconstです。
ポインタiThisのポイント先を変更できないと宣言されています。
class Foo { int mData[1000]; public: Foo() : mData{} { } static int get(Foo*const iThis, size_t idx) { return iThis->mData[idx]; } }; void Bar(Foo const& iFoo) { std::cout << Foo::get(&iFoo, 0) << "\n"; }
get()のパラメータに着目して下さい。
iFooはFoo型へのconst参照ですので、iFooの中身を書き換えないと宣言しています。
しかし、これを受け取っているget()ではFoo*と宣言しておりconstが付いていません。つまり、書き換えるかも知れないと宣言しているわけです。get()関数の内容は明らかにFooの中身を書き換えていないですが、そこまでコンパイラは見てくれません。つまり、コンパイラとしては宣言通り「書き換えるかもしれない」と解釈します。
従って、書き換えないと宣言している変数を、書き換えるかもしれない引数へ渡しているので「これは間違いでしょう」とエラーにするわけです。
そこで、get()が受け取る隠しパラメータのthisはconstであると宣言すればOKです。
class Foo { int mData[1000]; public: Foo() : mData{} { } static int get(Foo const*const iThis, size_t idx) { return iThis->mData[idx]; } };
これを元の非staticなメンバ関数で表現する場合、関数の「後ろ」にconstと記述します。
class Foo { int mData[1000]; public: Foo() : mData{} { } int get(size_t idx) const { return mData[idx]; } };
Wandboxで試してみるメンバ関数get()の後ろにあるconstを削除してみて下さい。
4.constとオーバーロードについて
constは型を修飾しています。オーバーロードは引数の型で呼び分けられます。
この性質を利用して、constの有無でメンバ関数を呼び分けることができます。
constがあるかないないかで呼び分けるケースとしてよく見かけるケースを例にとって説明します。
前章のように隠しthisポインタがconstインスタンスを指すかそうでないかで呼び分けるによく使われます。それは、その関数がメンバ変数への「参照」を返却する場合です。thisポインタがconstインスタンスを指している時はconst参照を返却することで変更不可とし、そうでない時は通常の参照を返却して変更可とするわけです。
std::vectorの超簡易版を作ってみます。
#include <iostream> #include <stdlib.h> class SimpleVector { static const size_t increase=16; int* mPointer; size_t mCount; size_t mSize; public: SimpleVector() : mPointer(nullptr), mCount(0), mSize(0) { } void push_back(int iData) { if (mCount < (mSize+1)) { mCount += increase; mPointer=(int*)realloc(mPointer, sizeof(int)*mCount); } *(mPointer+mSize)=iData; ++mSize; } int& operator[](size_t idx) { std::cout << "NonConst:"; return *(mPointer+idx); } int const& operator[](size_t idx) const { std::cout << "Const :"; return *(mPointer+idx); } size_t size() const { return mSize; } }; int main() { SimpleVector aSimpleVector; aSimpleVector.push_back(123); aSimpleVector.push_back(456); aSimpleVector.push_back(789); for (size_t i=0; i < aSimpleVector.size(); ++i) { std::cout << "aSimpleVector[" << i << "]=" << aSimpleVector[i] << "\n"; } aSimpleVector[1]=55555; std::cout << "aSimpleVector[1] chaged\n"; SimpleVector const& aSimpleVectorRef=aSimpleVector; for (size_t i=0; i < aSimpleVectorRef.size(); ++i) { std::cout << "aSimpleVectorRef[" << i << "]=" << aSimpleVectorRef[i] << "\n"; } }
まず、operator[]は第31回目でもちょっと触れましたが演算子を定義しています。operator[]は配列の添字演算子です。
さて、このoperator[]はmPointerの指定インデックスへの参照を返却します。
const版と非const版の2つを定義しています。これはメンバ変数への参照を返却するため、受け取った隠しパラメータのthisがconstなのに通常の参照を返却すると変更できてしまいます。それは好ましくないのでconst参照で返却しています。
そして、もし、const版しか定義しなかった場合、aSimpleVector[1]=55555;
のように要素を書き換えることができなくなります。もちろん、常に書き換えない場合はconst版だけでも良いのですが、このケースのように変更が必要な場合は、通常の参照を返却するバージョンも必要になります。
この2つはconstが有るかないかだけの微細な差しかないので、DRY原則(同じものを複数回書かない)に反するのであまり好ましくないのですが、現在のところこのようにほぼ同じものを2つ書くしかありません。
最後に、size()関数はconst版しか用意していません。非constな変数をconst変数へ設定することはできますので、隠しパラメータiThisについて非constなインスタンスをconstな変数を指すiThisへ渡すことができますからsize()関数はconst版しかなくても機能します。
演算子のオーバーロードについて
演算子のオーバーロードはソースを読みやすくする便利なテクニックです。しかし、プログラムを高速化するための機能ではありません。
ですので、当入門講座では深い説明は割愛致します。といいますか、演算子オーバーロードの解説は「C++マニアック」が優れていると思います。このリンク先を参照下さい。
5.const参照は一時オブジェクトを受け取ることができる!!
参照は原則として変数しか受け取れません。一時オブジェクトを受け取れないなのです。コンピュータの仕組み的には受け取ることもできますが、一時オブジェクトはその関数を呼び出している式が終わると消えます。その消え去る変数の値を変更しても意味がないから受け取らないということのようです。
しかし、const参照は変更することを許しませんから、受け取っても問題ないわけです。
5-1.const参照の場合
色々なケースで使われますが、例えば、文字列を受け取る時にもよく使われます。
#include <iostream> #include <string> void foo(std::string const& iString) { std::cout << "foo(" << iString << ");\n"; } int main() { foo("Calling foo"); }
std::stringはchar const*
を受け取るコンストラクタを持っています。そして、”Calling foo”は実引数としてはchar const*
型になります。ですので、foo("Calling foo");
を呼び出す時、まずstd::string(“Calling foo”);が呼ばれてstd::string型の一時オブジェクトが生成されます。
そして、std::string const&は一時オブジェクトを受け取れるので、意図通り文字列がfooへ渡されます。
もちろん、void foo(std::string iString)
と宣言しても文字列を渡すことができますが、この場合に次のように普通にstd::stringが渡されると、それがコピーされてしまいます。foo内部で文字列へ書き込まないのであれば勿体無いですね。
std::string aString("test"); foo(aString);
ですので、受け取った引数を変更しない場合、大きな変数は型名 const&
で受け取ると効率が良いです。
これにより、普通の変数を受け取る際にコピーしないので高速です。そして、上記のstd::stringのように一時オブジェクトを渡すこともでき、たいへん便利なのです。(ただし、int型やshort型等の1機械語命令でコピーできるような小さな型は、逆に参照にしない方が効率が上がります。)
5-2.通常の参照の場合
ちなみに、std::stringのchar const*
を受け取るコンストラクタは、上述したようにchar*
型も受け取れます。char*
はconstがないので なんとなくchar*
を渡すとそれを通常の参照で受け取れそうな気がするかも知れません。しかし、残念ながら、std::stringの一時オブジェクトが生成されると言う事実は変わりないため、一時オブジェクトを受け取れない通常の参照では受け取れません。
#include <iostream> #include <string> void foo(std::string const& iString) { std::cout << "foo(" << iString << ");\n"; } void bar(std::string& iString) // const無しの通常の参照 { std::cout << "bar(" << iString << ");\n"; } int main() { { foo("Calling foo"); char aStr[]="Calling foo"; foo(aStr); std::string aString("Calling foo"); foo(aString); } { #if 0 bar("Calling bar"); // これはエラーになる char aStr[]="Calling bar"; bar(aStr); // これもエラーになる #endif std::string aString("Calling bar"); bar(aString); // これだけ通る } }
Wandboxで試してみる#if 0を#if 1へ変更してみて下さい。
6.まとめ
今回は長らく説明を保留していたconstについて解説しました。
少し駆け足だったかも知れませんが、一通りconstについて解説できたと思います。
constは型を修飾するもので、constが付いた型の領域へ書き込むとエラーになる一種のフール・プルーフです。人はミスを犯すものですし、デバッグとは要するにミスを見つけて潰す作業ですから、それが少しでも楽になるとありがたいです。ちょっと使い方が難しいですが、是非constの使い方をマスターされて下さい。
さて、来週はお盆ですのでお休みさせて頂きます。次回は8月20日を予定しています。
次回はC++スタイルのキャストについて解説します。型情報を最小限しか失わないのでCスタイルの問答無用なキャストよりかなり安全です。お楽しみに。
Pingback: constの使い方について | ITedite