やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

C++の参照について使い方を確認してみた

C++の参照について使い方を確認してみた。

わかっているようで全くわかってない?

一応、使い方は理解しているつもりだった。 でも、classのメンバに持たせるときや関数の引数にするとき、コンパイルが通らないなど思った挙動にならないことがあった。よくわからないけど、ポインタにするとコンパイルが通るので、とりあえずポインタにしている状態。 これはいかん。

ポインタでなく参照にしたい

既存のclassインスタンスのメソッドを呼ぶだけなら参照でも十分なはず。

なんとなく参照のほうが安全なイメージがある。 ポインタ変数はアドレス操作ができてしまう。 アドレス操作なんてしたくないし、する必要もないはず。 なので、できればポインタを参照にしたい。

また、ポインタだとアロー演算子にせねばならないのが嫌。 ポインタ型と値型の違いを意識したくない。 そう思うのはC#をやっていたせいかもしれないが。

とりあえず参照を使ってコードを書いてみた

Ideoneというサイトでブラウザ上からC++を実行できる。 なんて便利なんだ。 これで参照の使い方を試してみた。

左辺値参照(ローカル変数)

継承の参照

Has-a関係と参照

VC++でも書いてみた

GitHub MEGA

思い出した

ほぼ思ったとおり。

あれ、一体何が問題だったんだっけ? と思ったけど思い出した。

たぶん昔、以下のように書いてエラーになった。

class B {};
class A
{
public:
    A(B& b) : m_b(b) {}
private:
    B m_b;
};
int main() {
    B b;
    A a(&b);
    return 0;
}

コンストラクタの引数では&をつけているのに、メンバ変数宣言では&をつけていない。

たぶん、以下のようなエラーがVC++でも出たのだろう。 それをみてポインタに変更したんだろう。よく覚えてないけど。

error: no matching function for call to 'A::A(B*)'
  A a(&b);
        ^

もし参照でやりたいなら、以下のようにやればよかった。

class B {};
class A
{
public:
    A(B& b) : m_b(b) {}
private:
    B& m_b;
};
int main() {
    B b;
    A a(b);
    return 0;
}

メンバ変数にも&をつけてやる。

&

ポイントとなるのは、&記号の使い方。

変数を宣言するときに&をつけると参照型の変数として宣言する意味になる。 でも、既存の変数に&をつけると、その変数のアドレス値を取得する意味になる。

A a(b);というところで、値渡しなのか参照渡しなのかの見分けがつかない。 これのせいでややこしく見える。

A a(&b);とかならわかりやすいが、それだと実際はアドレスを返すことになる。 だから引数側はA(B* b)でないと受け取れない。

初期化

また、参照型の変数は、初期値を代入せねば使えなかったはず。 なのに、classのメンバ変数では初期値を代入せずに宣言できる。 それは知らなかった。

というか、初期値を設定せねばならないなら、ほとんど参照変数の意味がなくなってしまう。 以下のような使い方しかできないということになってしまう。 別名定義くらいの意味しかなくなってしまう。それじゃ困る。他のclassや関数など異なるスコープの変数を参照できなければ存在意義が薄まる。

class A
{
public:
    A() {}
private:
    B m_b;
    B& m_rb = m_b;
};

なので、ローカル変数のときと宣言時のルールは違うけど、classメンバのときは初期化せずに宣言できるのだろう。 たぶん。

const参照

ところで、気になるのがconst参照での記述。

class B {};
class A
{
public:
    A(const B& b) : m_b(b) {}
private:
    B& m_b;
};
int main() {
    B b;
    A a(b);
    return 0;
}

以下のようなエラーになる。

error: binding 'const B' to reference of type 'B&' discards qualifiers
  A(const B& b) : m_b(b) {}
                       ^

たしか元々、GoogleC++スタイルガイドとかでconst &を知ったはず。 で、よく考えもせずに引数をconst &にしていた気がする。

  • const &は参照先の値を変更できないようにする(コンパイルエラーにさせる)

べつに今回のコードは参照先の値を変更していないはず。 なのに、なぜエラーになるのか。 英語よめないからエラーの意味も良くわからない。

というか、このコードはどういう意味になるんだ? 考えてみる。

考えてみる

B b;で値型として変数を用意した。 その値を参照する意味がA a(b);。 たぶんこれはローカル変数での宣言方法で書いたら以下のようなことなのだろう。

B b;
B& m_b = b;

で、constをつけると以下のようになる。

B b;
const B& m_b = b;

でも、これだとコンパイルが通った。

もう少し考えてみる。 関数は仮引数である。仮引数をconst B& bとしている。 仮引数の値を変更しないようにする、という意味のはず。 で、仮引数bの値は、main関数で宣言したB b;でなる。

ただ、変数名は同じだが、両者は異なるメモリ領域をもった変数のはず。 それをローカル変数で再現してみると、以下のコードになる。

B b;                        // main関数で宣言した値型
const B& provisionalB = b;  // A class のコンストラクタの仮引数。`A a(b);` `A(const B& b)`
B& m_b = provisionalB;      // A class のメンバ変数。`:m_b(m)`

おなじようなエラーがでた。

error: binding 'const B' to reference of type 'B&' discards qualifiers
  B& m_b = provisionalB;
           ^

google翻訳

タイプの参照「 B & 'に'定数B 'を結合修飾子を破棄します

たぶんこれと同じことが起こっている。 これがどういうことなのか理解できれば謎は解けるはず。

"type"とあるから型が一致しないということかもしれない。 const B&B&の型は違うから代入できないということかも。 以下のように無理やりキャストしてみたらどうなるか。

B b;                        // main関数で宣言した値型
const B& provisionalB = b;  // A class のコンストラクタの仮引数
B& m_b = (B&) provisionalB;      // A class のメンバ変数

コンパイル成功した。 つまり、const B&B&の型は違うから代入できないというエラーなのかな?

参照の参照?

B b;                        // main関数で宣言した値型
const B& provisionalB = b;  // A class のコンストラクタの仮引数。`A a(b);`
B& m_b = provisionalB;      // A class のメンバ変数。`:m_b(m)`

ところで、これはポインタのポインタならぬ、参照の参照になっていないだろうか。

なっていなかった。というか、そんなものはない。

class B {};
int main() {
    B b;
    B& provisionalB = b;
    B& m_b = b;
    printf("&b = %x\n", &b);
    printf("&provisionalB = %x\n", &provisionalB);
    printf("&m_b = %x\n", &m_b);

    B* p1 = &b;
    //B* p2 = p1;
    B* p2 = &p1;
    printf("p1 = %x\n", p1);
    printf("p2 = %x\n", p2);
    return 0;
}

以下のエラーになる。

error: cannot convert 'B**' to 'B*' in initialization
  B* p2 = &p1;
           ^

ポインタのポインタにするためにはB**のように*を2つ書かねばならない。 おなじように参照をB&&と書けば右辺値参照になる。参照の参照などない。

値のconstと参照のconst

何か勘違いしていたかもしれない。

const int v = 100;というのがあると、v = 200;がエラーになることはわかっていた。 でも、int v2 = v;がエラーになるとは思っていなかった。 実際、エラーにはならない。

でも、これが参照になると話が変わるということか。

考えてみれば、当たり前かもしれない。 参照とは、既存の変数を参照し操作するものである。 もし、constにしているのに、constでない変数に代入できてしまったら、せっかくconstにした意味がない。 constでないほうに代入してから、値を変更されてしまったら、constな変数の立つ瀬がない。 そういうことかもしれない。

値のほうが代入できたのは、異なるメモリ領域だから。vv2は異なるメモリ領域にあるため、int v2 = v;をしたあとでv2 = 200;としても、v2の値がかわるだけでvの値は変わらない。だから値型の場合はint v2 = v;としてもOK。

ところが、参照型の場合はそうはいかない。

int v = 100;
const int& r = v;
int& r2 = r;
r2 = 200;

error: binding 'const int' to reference of type 'int&' discards qualifiers
     int& r2 = r;
               ^

もし、これができてしまったら、せっかくrconstだったのに、constでないr2によって値が変更されてしまう。

では、もしメンバ変数のほうもconstなら成功するのだろうか。 そもそも、初期化しないで宣言できるのか。 試してみたら、できた。

class B {};
class A
{
public:
    A(const B& b) : m_b(b) {}
private:
    const B& m_b;
};
int main() {
    B b;
    A a(b);
    return 0;
}

見た目どおりでわかりやすい。 コンストラクタの仮引数(const B& b)とメンバ変数const B& m_b;は同じような宣言。 これを同じ型だとみれば、素直に理解できる。

ただ、当時こういう発想にならなかったのには理由がある。 たしか「純粋仮想関数classを継承した複数のclassのうち、どれか一つを参照したい」という状況だったと思う。 つまりメンバ変数がconstだと困る。実行時に変わってほしいものだから。

で、「メンバ変数の参照先は実行時に変更するけれど、仮引数が参照する値は変更しない」って宣言可能? それを表現したつもりのコードが、以下のエラーになったコード。

class A
{
public:
    A(const B& b) : m_b(b) {}
private:
    B& m_b;
};

でもこれは異なる型としてエラーになってしまう。

仮引数への代入を防げないか

その関数内部で仮引数に代入させないような宣言はできないのか。 constではできそうにない。 http://nonbiri-tereka.hatenablog.com/entry/2014/07/04/095118

基本的に、仮引数に代入なんてするつもりはない。 でも、できてしまうことを防ぐことに意味がある気がする。 「仮引数が変更されていないか」という点を注意してコードを読まずに済む。 勘違いして変更してしまっても、コンパイルエラーになってくれたらバグらずに助かる。 assert的な意義がある気がする。

そもそも、「仮引数に代入できないようにしたい」という発想がおかしいのだろうか? 仮引数にも代入できたほうが便利とか? 仮引数だけ代入禁止なんてことを実装できるようにしたらわかりづらくなるとか?

いずれにせよ、そんな機能は見つけられなかった。

参照よりポインタのほうがいいかもしれない

ここまでさんざん参照のことをやってきたが、ポインタのままのほうがいいかもしれない。

ヘッダファイルで保持するクラスを前方宣言することにより、includeファイルを減らせる。これによってコンパイル時間を短縮できる。

でもこれはポインタ変数で宣言せねばならない。値型も参照型も使えない。

http://ota42y.com/blog/2014/09/22/cpp-include/

規模が大きくなると、かなり重要になる。

なんでファイルの依存関係のせいで変数の型を変えねばならないのか納得しかねる。 しかもビルド時間を短縮するというコード内容と直接関係ない理由。 すごくやりたくない。関係ないところでコードが汚されていく。 でも、事実上は必須と思われる。 規模次第では現実的なコンパイル時間に納めるために必須っぽい。 今のところはまだそこまで困っていないが。

つまり、ここまで参照をやったけど、将来的にポインタのままにしていたほうが修正が少なくて済むかもしれない。

解放処理を考えると、スマートポインタにするほうがいいかもしれない。 そっちも使い方がわからない。