参照 | Programming Place Plus Modern C++編【言語解説】 第12章

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要 🔗

この章の概要です。


左辺値と右辺値 🔗

この章のテーマである参照について説明する前に、左辺値右辺値について触れておきましょう。

正確ではありませんが、左辺値とは、基本的には名前が付いているもののことです。右辺値はその逆で、名前がないもののことです。代入演算子の左側に来るものだとか、右側に来るものだとかは何ら関係がないことに注意してください。左辺値が右側に来ることは頻繁にあります。

【上級】C++11 の定義では、左辺値と右辺値は細分化することができ、lvalue、rvalue、glvalue、xvalue、prvalue に分類されます。通常、そこまで細かい理解が必要になることはないので、Modern C++編では、左辺値と右辺値の分類のみで説明しています。

いくつか例を挙げてみます。

int func()
{
    int x = 10;
    return x;   // x 自体は左辺値
}

int main()
{
    int a, b;

    a = 0;       // a は左辺値、0 は右辺値
    b = a;       // b は左辺値、a も左辺値
    a = func();  // a は左辺値、func() が返す値は右辺値
    b = a + 5;   // a と b は左辺値、5 は右辺値。「a + 5」は右辺値
}

変数a は「a」という名前があるので、代入演算子の左側に登場しようと、右側に登場しようと左辺値です。変数b も同様です。一方、「0」のような定数は、名前がないものなので右辺値です。

分かりにくいものがいくつかあります。まず、文字列リテラルは左辺値です。型としては const char[] ですから、書き換えることはできません。

また、関数の戻り値は通常、右辺値です。ただし、この章のテーマである参照を使うことで、左辺値を返すことができます。

大体の見分け方は分かったとして、実用上の違いはどこにあるのでしょうか。大きな違いとして、右辺値は変更できません。左辺値の場合は、変更できることもあるし、できないこともあります。変更できない左辺値の分かりやすい例は、const が付いている変数です。それ以外に、後で取り上げる参照も、左辺値ですが変更できません。

いくつか例を挙げておきます。

int func()
{
    int x = 10;
    return x;
}

int main()
{
    int a = 5;
    const int b = 10;

    0 = a;       // 0 は右辺値なのでエラー
    3 + 5 = a;   // 3 + 5 は右辺値なのでエラー
    func() = a;  // func() は右辺値なのでエラー
    a = 10;      // a は const無しの左辺値なので OK
    b = 10;      // b は const付きの左辺値なのでエラー
}

左辺値、右辺値という用語を知らなくても、経験的に理解できる範疇だと思います。

参照 🔗

C++ には、参照(リファレンス)という機能があります。参照とは、何らかのものに対して与えられた別名(エイリアス)ですが、ある意味で限定的なポインタのように利用できます。具体的なことは、この後の項で取り上げます。

また、C++11 になって、右辺値参照という、参照の一種が追加されました。これについては第14章で取り上げますが、右辺値参照との区別を付けるため、C++11 より以前からある従来の参照は、左辺値参照と呼ばれるようになりました。このように現在では、参照には複数の意味があります。

なお、参照そのものは、何かの別名という形で名前を持っていますから左辺値です。

左辺値参照 🔗

左辺値参照は、その名のとおり、左辺値を参照するために使われる機能です。左辺値参照は、「参照するものの型」に「&」を付加して表現します。たとえば、「int&」は int型の左辺値を参照する左辺値参照の型名です。

型名& 変数名 = 初期化子;

具体的には次のように書きます。

int num = 100;
int& ref = num;    // 左辺値 num を参照する
int num2 = ref;    // ref(=num)を使って num2 を定義

const int cnum = 100;
int& ref2 = cnum;  // コンパイルエラー。const付きの左辺値は参照できない

const が付いている左辺値を参照する場合は、参照の方も const付きでなければなりません。const付きの左辺値参照については、後の項でも取り上げています

前の項での説明のように、そもそも参照とは別名のことなので、先ほどの例で言えば、ref は左辺値 num の別名です。これが意味することは、次のプログラムで説明できます。

#include <iostream>

int main()
{
    int num = 100;
    int& ref = num;

    std::cout << num << std::endl;
    std::cout << ref << std::endl;

    ref -= 10;
    std::cout << num << std::endl;
    std::cout << ref << std::endl;

    num -= 10;
    std::cout << num << std::endl;
    std::cout << ref << std::endl;
}

実行結果:

100
100
90
90
80
80

ref は num の別名なので、ref の値を変更しても num の値を変更しても同じことですし、どちらの値を出力してみても同じ結果を得られます。これは、ポインタの挙動と似ていますが、メモリアドレスを取得するための「&」や、間接参照のための「*」は登場しません。

ポインタで使う「->」も不要です。参照を使って、構造体やクラスのメンバを使う場合には、「.」を使います。

#include <iostream>

struct Data {
    int num;
};

int main()
{
    Data data;
    Data& ref = data;

    ref.num = 100;
    std::cout << data.num << std::endl;
    std::cout << ref.num << std::endl;

    data.num = 200;
    std::cout << data.num << std::endl;
    std::cout << ref.num << std::endl;
}

実行結果:

100
100
200
200

ポインタと違って、参照を使った場合には、構文上の変更もなく、参照先のものとまったく同一であるかのように扱えます。ポインタの使われ方(の一部)は、参照で置き換えることが可能です。参照ではできない代表的なことには、以下のものがあります。

  1. ポインタ同士で差分を取るような、アドレス計算には使えない
  2. 「参照の参照」のような使い方はできない

また、必ず何かの別名であるように初期化しなければならないので、「何も参照していない参照」を作ることはできず、ヌルポインタに相当する考え方はありません。これは意外と便利で、ヌルポインタかもしれないという考えを排除して、プログラムを記述できるようになり、ヌル判定の if文や assert をなくせます。

「int**」で「ポインタのポインタ」になるような、「参照の参照」のようなものはありません。「int&&」という型はありますが、これは違う意味になります。

#include <iostream>

int main()
{
    int a = 20;
    int& r = a;
    int& r2 = r;  // int&& ではない

    std::cout << r2 << std::endl;
    a = 30;
    std::cout << r2 << std::endl;
}

実行結果:

20
30

参照は単に参照で受け取れます。r2 は r の別名であり、r は a の別名ですから、a の値を変更した後、r2 を使っても変更後の値が確認できています。

参照渡し 🔗

参照を使うと、ポインタと同様に、大きなオブジェクトをコピーする際の負荷を避けられます。

class MyClass {};  // 巨大なクラス

void func(MyClass& ref);

int main()
{
    MyClass a;
    func(a);  // func関数の仮引数は参照
}

引数に参照を渡す形は、参照渡しと呼ばれます。MyClass型のオブジェクトのコピーは作られることなく、func関数の中で ref を使って a にアクセスできます。

前の項で取り上げたように、参照にはヌルポインタに相当するものがないので、ヌルでないかどうかをチェックする必要がありません。

#include <cassert>

class MyClass {};  // 巨大なクラス

void func(MyClass* ptr)
{
    assert(ptr != nullptr);  // ptr はヌルポインタの可能性がある
}

void func(MyClass& ref)
{
    // ref は必ず有効な何かを参照している
}

ところで、ここまでのサンプルプログラムで使っている参照は左辺値参照ですから、右辺値を渡すことはできません。

void func(int& ref)
{
}

int main()
{
    int n1 = 10;
    const int n2 = 20;

    func(n1);    // OK。n1 は左辺値
    func(n2);    // OK。n2 は左辺値
    func(30);    // コンパイルエラー。30 は右辺値
}

func関数が ref をどう使うかによりますが、ref を経由して書き換えを行うのであれば、「30」のような定数をが渡せないのはむしろ適切であると言えます。

一方、ref を経由した書き換えを行わないのであれば、いつものように const の出番です。func関数の仮引数を const int&型にすれば解決します。

void func(const int& ref)
{
}

int main()
{
    int n1 = 10;
    const int n2 = 20;

    func(n1);    // OK。n1 は左辺値
    func(n2);    // OK。n2 は左辺値
    func(30);    // OK。30 は右辺値だが認められる
}

const付きの左辺値参照(const参照)は特殊で、右辺値であっても参照できます。const参照は C++ の古い規格の頃からあるものですが、C++11 で右辺値参照(第14章)が追加されて、参照するものの区別が必要になったことで、少々不自然な仕様になってしまっています。

const参照については、この後で詳しく取り上げます

参照戻し 🔗

参照型の戻り値を返すことを、参照戻しと呼びます。

int& func()
{
    int n = 100;

    return n;
}

int main()
{
    int& r = func();  // OK。参照のまま
    int a = func();   // OK。コピーを作る
}

参照型の戻り値を参照型の変数で受け取ることは当然できますし、参照でない変数を使って受け取ることも可能です。後者の場合は、コピーされます。

ところで、このサンプルプログラムには問題あります。ポインタの場合と同じで、func関数内のローカル変数 n は、関数を抜け出した後には存在しないことが原因です。存在しないものの別名を使うことはできず、未定義の動作です。

変数 a のように、コピーで受け取っておけば問題はありません。また、変数 n が静的ローカル変数である場合も問題ありません。

通常、関数の戻り値は右辺値として返されますが、参照型の戻り値は左辺値です。そのため、返された参照にそのまま代入することが可能です。

#include <iostream>

int& func()
{
    static int n = 100;

    std::cout << n << std::endl;

    return n;
}

int main()
{
    func() = 200;  // OK
    func();
}

実行結果:

100
200

もちろん、先に述べたとおり、func関数内のローカル変数を参照しているのなら、static にしておかないと未定義の動作になってしまいます。

const参照 🔗

左辺値参照に const修飾子を付加できます。このような参照を、const参照と呼びます。

const 型名& 変数名 = 初期化子;

const参照は、constポインタと同様で、参照先の値を変更できません。また、クラス型のオブジェクトを参照している場合には、非constメンバ関数を呼び出すこともできません。

class MyClass {
public:
    void f1() {}
    void f2() const {}
    int v;
};

int main()
{
    MyClass a;
    MyClass& r = a;
    const MyClass& cr = a;

    r.f1();    // OK
    r.f2();    // OK
    r.v = 0;   // OK

    cr.f1();   // コンパイルエラー。非constメンバ関数は呼び出せない
    cr.f2();   // OK
    cr.v = 0;  // コンパイルエラー。書き換えられない
}

const参照は、左辺値参照であるのにも関わらず、右辺値を参照できます。この機能は、関数から右辺値を受け取る際に利用できます。

int func()
{
    int n = 100;
    return n;  // n が戻り値にコピーされる
}

int main()
{
    int& r = func();         // コンパイルエラー
    const int& cr = func();  // OK
}

func関数は、非static なローカル変数 n を返しています。ここできちんと把握しておくべきなのは、実際に func関数の呼び出し元へ返されているものは、n そのものではなく、n をコピーしたものだという点です。

n をコピーして作られた戻り値は、名前が付いていない右辺値です。戻り値は、「int x = func();」のようにすれば、x にコピーされますが、いずれにしても戻り値自体は消えてしまいます。

戻り値自体は消えてしまうという部分で疑問が生まれます。const参照を使えば、右辺値を参照できるとのことですが、消えてしまうものを参照して問題ないのでしょうか?

ここに特別なルールがあります。const参照によって参照された右辺値は、その const参照自身が存在している限り、消えずに残り続けます。この挙動を、参照による束縛といいます。

そのため、次のプログラムは問題なく動作します。

#include <iostream>

int func()
{
    int n = 100;
    return n;  // n が戻り値にコピーされる
}

int main()
{
    const int& cr = func();  // 右辺値を束縛
    // 本来は、func() の戻り値は消えてしまうはずだが、束縛されているので延命する

    std::cout << cr << std::endl;  cr の参照先は生存し続けている
}

実行結果:

100

といっても、右辺値を書き換えることはできません。そもそも、const参照ですから、書き換えられません。const_cast で非const の参照に変換できますが、それをしたからといって、右辺値を書き換える行為が安全になる訳でもありません。


配列の参照 🔗

配列は名前があるので左辺値ですが、参照するのは意外と難しく、次のような記述になります。

int array[10];
int (&ref)[10] = array;

このように、参照の側にも要素数を含む必要があります。これはつまり、要素数が異なる配列は参照できないことを意味していますが、これを利用して、特定の要素数を持った配列だけを受け取れる関数を作れます。

void func(int (&array)[3]);

int main()
{
    int array1[3] = {0, 1, 2};     // OK
    int array2[4] = {0, 1, 2, 3};  // コンパイルエラー

    func(array1);
    func(array2);
}

このように、配列の参照は要素数に応じた型になりますが、これを活かして、配列の要素数を取得する関数テンプレートを作れます。

#include <iostream>

template <typename T, std::size_t SIZE>
inline std::size_t sizeOfArray(const T (&array)[SIZE])
{
    return SIZE;
}

int main()
{
    int array1[3] = {0, 1, 2};
    int array2[4] = {0, 1, 2, 3};

    std::cout << sizeOfArray(array1) << std::endl;
    std::cout << sizeOfArray(array2) << std::endl;
}

実行結果:

3
4

関数テンプレートのテンプレート仮引数は、可能であれば実引数から自動的に判断されます。参照を使っていれば、要素数が一致することも必要なので、テンプレート仮引数 SIZE についても自動判断されます。あとは、SIZE をそのまま return すれば良いです。

従来、配列の要素数を取得する関数形式マクロを使うことが多かったですが、C++ ではこういう方法もあります。

ポインタの参照 🔗

ポインタを参照することも可能です。

int a = 100;
int* p = &a;
int*& r = p;

この場合、r は p の別名ですから、次のように r を使えます。

int main()
{
    int a = 100;
    int* p = &a;
    int*& r = p;

    std::cout << *r << std::endl;

    *r = 200;
    std::cout << *r << std::endl;

    int b = 300;
    r = &b;
    std::cout << *r << std::endl;
}

実行結果:

100
200
300

ややこしく感じるかもしれませんが、r が p の別名であるということを意識して考えると良いです。いつも、r と p は置き換え可能なのです。

なお、これとは反対の、参照へのポインタを表す型はありません

int a = 100;
int& r = a;
int&* p = &r;  // コンパイルエラー

参照へのポインタが必要なときは、単にポインタ型にすれば良いです。

int a = 100;
int& r = a;
int* p = &r;  // OK


練習問題 🔗

問題① 次の中から、左辺値参照(const参照でない)で参照できるものを選んでください。

問題② 次のプログラムの実行結果を答えてください。

#include <iostream>

int func()
{
    static int n = 0;

    n++;
    return n;
}

int main()
{
    const int& cr = func();
    std::cout << cr << std::endl;

    const int& cr2 = func();
    std::cout << cr << std::endl;
    std::cout << cr2 << std::endl;
}


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 C++編【言語解説】第16章「参照」の修正に合わせて、内容更新。

 新規作成。



前の章へ (第11章 関数テンプレート)

次の章へ (第13章 コピー)

Modern C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る