Modern C++編【言語解説】 第12章 参照

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

左辺値と右辺値

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

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

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」のような定数は、名前が無いものなので右辺値です。

func() という関数呼び出し式はどうでしょうか?
func() の実装を見ると、変数x の値を返しており、変数x 自体は名前のある左辺値です。しかし、実際に返される値は「変数x のコピー」であって、変数x そのものではありません。その「コピー」の方には名前がありませんから、これは右辺値です。よってfunc() という関数呼び出し式は右辺値となります。戻り値を受け取った変数 a は左辺値です。

ここで作られている「コピー」のことを、一時オブジェクトと呼びます。一時オブジェクト自体も C++ の理解のために重要な概念なので、第13章で改めて解説します。

大体の見分け方は分かったとして、実用上の違いはどこにあるのでしょうか。
大きな違いとして、右辺値は変更することができません。左辺値の場合は、変更できることもあるし、できないこともあります。変更できない左辺値の分かりやすい例は、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. ポインタ同士で差分を取るような、アドレス計算に使うことはできない。
  3. 「参照の参照」のような概念は無い。

参照は初期値を与えた後、変更することはできません。 これは、1度作られた参照は常に同じものを指しているので、処理の分かりやすさにつながります。

また、変更できないということは、必ず何かの別名であるように初期化しなければならないということの裏返しでもあります。そのおかげで、「何も参照していない参照」を作ることはできず、ヌルポインタに相当する考え方は存在しません。これは意外と便利で、ヌルポインタかも知れないという考えを排除して、プログラムを記述できるようになり、ヌル判定の 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 main()
{
    int& r = func();  // OK。参照のまま
    int a = func();   // OK。コピーを作る
}

この場合の「func()」の呼び出しは、左辺値になります。そのため、左辺値参照を使って受け取ることができますし、参照でない変数を使って受け取れば、コピーを作ることになります。

int& func()
{
    int n = 100;
    return n;
}

ポインタの場合と同様で、このコードには問題あります。つまり、変数 n は関数を抜け出した後には存在しないので、n の別名としての参照を呼び出し元で使うと、未定義の動作になります。参照のまま受け取らず、呼出し元でコピーを取った場合は問題ありません。

int& func()
{
    int n = 100;
    return n;
}

int main()
{
    int& r = func();  // コンパイルは通るが危険
    int a = func();   // OK だが、あくまでもコピーである
}

設計の良し悪しの点では検討が必要ですが、n が static であったり、メンバ変数であったりすれば、参照で返して、参照で受け取っても構いません。

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;
}

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

右辺値には名前がありませんが、それは事が済んだら消えてしまうということでもあります。このサンプルプログラムで言えば、func() が返しているものは、ローカル変数 n のコピーですが、コピー自体は名前が無い右辺値です。呼び出し元が「int a = func();」のような感じで他の変数に受け取ったとすると、コピーから変数 a が作られますが、コピーはそれで役割を終えて消えて無くなります。

一方、「const int& cr = func();」のように、const参照で受け取った場合、コピーは延命されて、cr の方が消えるまで生き続けます。そのため、cr を使ってコピーにアクセスし続けることが可能です。このように、const参照によって、すぐに消えるはずの右辺値が延命される動作を、参照による束縛と言います。

#include <iostream>

int func()
{
    int n = 100;
    return n;
}

int main()
{
    const int& cr = func();  // 右辺値を束縛
    std::cout << cr << std::endl;  // const参照を使って、右辺値へアクセス
}

実行結果:

100

あくまでも「const」付きの参照なので、右辺値を書き換えられる訳ではありません。const_cast を使ったとしても、コンパイルエラーにはなりませんが、意図通りには動作しないでしょう。

配列の参照

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

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;
}


解答ページはこちら

参考リンク

更新履歴

'2017/8/7 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ