ホーム < ゲームつくろー! < C++踏み込み編

その17 constのあれこれ2

 const…このたった一つのキーワードは、ライブラリ全体の振る舞いを統括する恐るべき力を持っています。C++踏み込み編その2「constのあれこれ」ではconstの機能について見てきました。この章ではそれをおさらいしつつ、constが発するメッセージにさらに耳を傾ける事にしましょう。


@ constルール

 constは「その値を以後変更できなくする」という印です。ここでいう「その値」とは、文字通り変数に入っている値です。ですから、次のように宣言した値は一切変更ができません:

const int i0 = 100;
const int *pi0 = &i0;
const int &ri0 = i0;

i0   = 200;  // error!
*pi0 = 200;  // error!
ri0  = 200;  // erorr!

実体(i0)そして参照(ri0)はわかります。ただポインタは要注意です。上のようにポインタの先にある値を変更する事はできません。でも、ポインタ自体の入れ替えは「セーフ」なんです:

const int i0 = 100;
const int *pi0 = &i0;

const int i1 = 300;
const int *pi1 = &i1;

pi0 = pi1;   // これセーフ

ポインタの値自体も変更不可にするには、次のようにconstをもう一つ付けます:

const int i2 = 500;
const int* const pi2 = &i2;
pi2 = pi0;   // error!

こうするとそのポインタはポインタ自体も変更出来ませんし、参照先の値を変更することもできなくなります。かんなりガッチガチになるわけです。

 ただし、ポインタのconstには弱点があります。次のコードを御覧下さい:

struct Val {
    int a;
    int *pA;
   Val *child;
};

void testFunc1(const Val* val) {
    val->a = 300;            // @
   *val->pA = 300;          // A
    val->child->a = 300;     // B
}

int main() {
    Val v1;
    Val v2;
    v1.child = &v2;
    int a = 100;
    v1.a = &a;
    testFunc1(&v1);

    return 0;
}

Valという構造体の中に実体a、ポインタpA、そして構造体のポインタ*childがあります。testFunc関数の引数でconstを付けたValのポインタを渡してもらった時に、@、A、Bは全部エラーになるでしょうか?これは@は「constがついてるからaは変更できませんよ!」と怒られます。しかしAとBはセーフなんです。

 別の例を。次のようなダブルポインタを受け取る関数はうまく動くのでしょうか?

void testFunc2(const int **ppI) {
    *ppI = new int(100);
}

int main() {
    const int i0 = 500;
    const int *a = &i0;
    testFunc2(&a);
    delete a;

   return 0;
}

main関数の中でconst int*型の変数を一つ作り初期化しています。*a = 500です。constポインタなので参照先の変更はできません。例えば「*a = 600;」などとするとコンパイラに怒られます。しかしです。これをtest2Funcに渡しました。ここでポインタ変数a自体を書き換えてしまいました!戻ってきたaは別物で、参照先*aも100に変わってしまっています。このコード、ちゃんとコンパイルも通るし正しく動きます。

 つまり、ポインタのconstは、あくまでもそれが参照している先の値を直接変更しようとすると怒られるわけで、ポインタの先がポインタである時、手前にconstが付いていようがお構い無しにその先の値は変更できてしまうんです:



A 関数の引数としてのconst

 関数の引数にconstを付けた場合、その引数自体は変更できなくなります。下の例を御覧下さい:

void func(int a, const int b) {
    a = 400;
    b = 500; // error!
}

int main() {
    func(100, 200);
    return 0;
}

func関数の引数a及びbは値渡しですが、bはそれをconstとしてコピーします。そうすると、関数内では変更ができない値として扱えます。ですからbに代入しようとするとエラーになります。ここまでは簡単。

 続いてポインタの場合:

void func(int *a, const int *b) {
    *a = 400;
    *b = 500; // error!

    int c = 200;
    b = &c; // OK

}

int main() {
    int a = 100, b = 200;
    func(&a, &b);
    return 0;
}

constが付いたポインタ引数の場合、ポインタの先を変更しようとすると怒られます。でも、ポインタ変数そのものには違う値を代入できてしまいます。これも@の最後で説明したルールの通りです。では、文字列を引き渡す場合はどうか?

void func(const char* str) {
    str[0] = 'P';   // error!
}

int main() {
    func("Test");
    return 0;
}

今までのルールで考えると、cosnt付きポインタが引数場合、それが指す先の値の変更は不可でした。よって、上のように配列演算子を使って値を変更しようとしてもエラーとなってしまいます。引き渡した文字列を変更されたくない場合はconstを付ければ良いという事です。

 では「文字列の配列」の場合はどうでしょう。複数の文字列を引数に渡して、何かをしてもらうというのは良くある事です:

void func(const char** str) {
    str[0] = "P";     // OK!
    str[0][0] = 'P';  // error!
}

int main() {
    const char* str[] = {
        "Str1", "Str2", "Str3"
    };
    func(str);
    return 0;
}

ぶは!何ということでしょう、関数の中で引数で渡した文字列変更が許可されてしまいました。main関数の文字列配列は関数から返ると「"P", "Str2", "Str3"」という文字列に変わってしまいます。しかも"P"はfunc関数の中で引き渡されたローカルな場所にあるため、いずれかき消されます。ダブルポインタのconstがガードするのは実体だけなんです:

これはトリプルポインタ以降も同じです。では上のポインタ先をガードするにはどうするか?次のような引数に変更します:

void func(const char* const *str) {
    str[0] = "P";     // error!
    str[0][0] = 'P';  // error!

    const char *pS = "Test";
    const char** ppC = &pS;
    str = ppC;   // OK!!
}

int main() {
    const char* str[] = {
        "Str1", "Str2", "Str3"
    };
    func(str);
    return 0;
}

さぁ出ました、C言語の訳分からん世界。上のはどう読み取れば良いのでしょうか。一番最初に付いているconstは「ポインタの実体をガードしますよ〜」という意味です。char*型の次に来ているconstはその一つ前の値をガードしますよという意味になります。分かりにくーい(笑)。ですから、上の場合**str自体は変更ができてしまいます。

 んでは、strも変更しては駄目!っとしたい場合はどうするか?もうそろそろ察しがついていると思いますが、こうなります:

void func(const char* const* const str) {
    str[0] = "P";     // error!
    str[0][0] = 'P';  // error!

    const char *pS = "Test";
    const char** ppC = &pS;
    str = ppC;   // error!!
}

int main() {
    const char* str[] = {
        "Str1", "Str2", "Str3"
    };
    func(str);
    return 0;
}

ぶふっと笑ってしまいますが本当です。ところで、funcの引数はconst char* const* const str型…ですが(^-^;、引数に渡している元の方はconst char**型です。これ、どうして引渡しできているのでしょうか?関数に引き渡すときには、元の値をより固くガードするコピーはOKなんです。逆に元々constで守られていた変数をオープンにしてしまうような引渡しはNGなんです。ですから、上のconst祭りな引数はより堅牢にしているだけなのでOKです。でも、最初のconstを外したとすると、実体へのガードが無くなってしまうのでNG。なんとなく分かりましたでしょうか(^-^;;



B 関数の戻り値としてのconst

 constは関数の戻り値にも付ける事ができます。例えばこんな感じ:

const int func() {
    return 300;
}

int main() {
    int v = func();
    return 0;
}

main関数内ではfunc関数の戻り値をint型で受けています。これ、大丈夫なのでしょうか?実際にコンパイルは通ります。なぜか?constだった値をint型に「コピー」しているからOKなんです。もちろん、

const int func() {
    return 300;
}

int main() {
    const int v = func();
    return 0;
}

も合法。vを宣言と同時に初期化しているのでOKなんです。ただし、

const int func() {
    return 300;
}

int main() {
    const int v;
    v = func();       // error!
    return 0;
}

は駄目。vはconstなので宣言時に初期化しないとエラーになってしまうためです。constを外せばOK。では、int型の参照として受けるのはどうか?

const int func() {
    return 300;
}

int main() {
    int& v = func();    // error!
    return 0;
}

これはエラーです。どうしてか?戻り値はconst int型です。それをint型の参照にするというのは、戻り値のconstを外す行為です。ガードが切れるのでエラーというわけ。ですから、

(2013. 4. 3修正)
※ kariya_mitsuruさんにご指摘頂きました

 constの付いていない参照を数値等の右辺値でダイレクトに初期化する事はできません。上の式はいわば、

int& v = 300;

と初期化しようとしているようなもので、これは無理です。

 ここをconst int&型で受けるのはセーフ:

const int func() {
    return 300;
}

int main() {
    const int& v = func();    // OK!
    return 0;
}

int型の場合、参照で受けようがコピーしようが大した差は無いのですが、構造体の場合はコピーは避けたい所。その時、関数の戻り値にconstが付いていたら、受ける側もconstにしなければなりません:

const Val func() {
    Val val;
    val.a = 100;
    val.b = 300;
    return val;
}

int main() {
    const Val& val = func();    // OK!
   Val val2 = func();           // OK!

   Val& val3 = func();          // error!!

    return 0;
}

constを外したらもちろんエラー。constを外す場合は参照ではなくてコピーとして受けなければなりません。constを考えるときには常に「ガードを外す代入はエラー」と覚えておくとすっきりします。



C クラスのメソッドに付くconst

 constはクラスのメソッドにも付ける事ができます。そして、これがまた頭がごっちゃになってしまうんです。例えばこんな感じに付きます:

class MyClass {
    int val;

public:
    const int& getVal() const {
        return val;
    }
};

メソッドの最後にconstが付きます。これ、どういう意味になるかというと「メソッドの中でメンバ変数を変更できませんよ〜」という意味になります。ですから、

class MyClass {
    int val;

public:
    const int& getVal() const {
        val = 600;
        return val;
    }
};

などとメンバを変更しようとするとエラーになってしまいます。またそれだけでなく、参照を返す場合にはそれもconstであることを強要されます。ですから、戻り値のconstを外してもエラーになってしまうんです。中々に厳しいのです…。

 では、ポインタはどうなのでしょうか?MyClassにint*型のポインタ変数を追加してポインタ先を変更してみます:

class MyClass {
    int val;
    int *pVal;

public:
    MyClass() {
        pVal = &val;
    }
    const int& getVal() const {
        *pVal = 600;
        return val;
    }
};

これ「セーフ」なんです。「えーー!なんでじゃー」となりますが、セーフなんです。メソッドの後のconstは、あくまでもメンバ変数そのものの変更をガードしています。ですから、参照先の事などは知らんのです。

 メソッドの後ろのconstの機能として、もう一つ厳しいと言うか厳格なガードがあります。それは「constが付いたメソッドしか呼べない」という物です。MyClassにsetValメソッドを追加してgetValメソッド内で呼んでみます:

class MyClass {
    int val;
    int *pVal;

public:
    MyClass() {
        pVal = &val;
    }
    void setVal(int v) {
        val = v;
    }
    const int& getVal() const {
        setVal(100);    // error!
        return val;
    }
};

これは「error C2662: 'MyClass::setVal' : 'const MyClass' から 'MyClass &' へ 'this' ポインタを変換できません。」というエラーになります。これはですね、constが付いたメソッド内では、自分自身がconstが付いた状態に変化します。これが'const MyClass'です。それに対してsetValメソッドにはconstが付いていません。クラスのメソッドは暗黙の了解として第0引数にMyClass&という参照の引数を持っていて、メソッドを呼び出す時に自分自身を第0引数に引き渡しています(*thisが渡されている)。自分自身がconst付きなのに、第0引数はconstが付いていないのでガードが外れる方向の代入になるわけです。だからエラーとなってしまいます。いやぁー厳格。

 ですから、メソッドの後ろにconstを付けるというのは、非常に厳格にメンバを変更しないという覚悟の元で付けないといけないんです。

 メソッドとしてconstが付くメソッドと付かないメソッドは別物として取り扱われます:

class MyClass {
    int val;

public:
    const int& getVal() const {
        return val;
    }
   int& getVal() {
        return val;
    }
};

これ、どういう時に重宝するかと言うと、constが付いたメソッド内でも呼びたいし、付いていないメソッド内で引数の参照を通して値を変更したい場合に欲しいんです。そんな状況ってあるの?って思うかもしれませんが、例えばstd::mapのfindメソッドなどがそうです:

class MyClass2 {
    std::map<int ,int> valMap;

public:
    MyClass2() {
        valMap.insert(std::pair<int, int>(1, 100));
        valMap.insert(std::pair<int, int>(2, 200));
    }

    void func() {
        std::map<int, int>::iterator it = valMap.find(1);
        int &val = it->second;
    }
    void func() const {
        std::map<int, int>::const_iterator it = valMap.find(1);
        const int &val = it->second;
    }
};

MyClass2::funcメソッドはconst無しとconst付きの2タイプがあります。constが付いていない方ではメンバメソッドの変更が可能です。そのため普通のiteratorを通してマップの値を変更できます。追加もできます。所が、const付きのfuncメソッドの場合、iteratorでfindメソッドの戻り値を受ける事ができません。これはconst付きのmap:findメソッドが呼ばれて戻り値がconst_iteratorになっているためです。厳格に、値が変更できないイテレータで、secondもconst付きの値になっています。

 この2つのMyClass2::funcメソッドがどのように呼び分けられるかと言うと、MyClassオブジェクトがconstか否かで決まります:

MyClass2 obj2;
obj2.func();        // const無しのfuncが呼ばれる

const MyClass2 obj2_const;
obj2_const.func();  // const付きのfuncが呼ばれる

constが付いていないMyClass2オブジェクトだとconst無しのfuncメソッド、const付きのMyClass2オブジェクトだとconst付きのfuncメソッドがそれぞれ呼び分けられます。

 ですから、例えばオーバーロード演算子などはconst付きでもconst無しでも呼ばれる場合があるため、両方に対応した演算子をオーバーロードしておかないと変なことになります。



D const MyClass &val な引数の意味

 コピーコンストラクタの引数は「const MyClass &val」です。クラスにコピーコンストラクタを定義しなかった場合はコンパイラが自動的に作成します。

 コピーコンストラクタの引数にconstが付いている意味は、もうお分かりの通りで、コピー元を一切汚さない為です。コピー元を何も変更せずに、自分自身にコピー元の値を全部ディープコピーするのがコピーコンストラクタの役目です。一般変数(int型など)はそのまま値コピーしますし、クラスのオブジェクトなどはそのクラスが持つコピーコンストラクタを用いて値をコピーします。

 ただ、もうすでに私達は「参照クラスがポインタを持っていた場合、そのポインタの先は変更できる」という事を学びました。ですから、もしMyClassがint *vというポインタ変数を持っていたら、題目の引数であっても「*val.v = 400;」という代入はできてしまいます。これをうまく利用すると、スマートポインタの参照カウンタなどを操作できるわけです。


 constのあれこれ、どうでしたでしょうか?変更したくても出来ないというのは、頑健なライブラリには必要になる事もあります。でも、それは一方で強固な束縛でもあります。constの付け方でライブラリの全体の流れや機能すら決められてしまうというのは、まさにconstパワーの恐ろしさなのです。疎かに扱うと泣きを見るのがconstです。