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

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


lvalue と rvalue

この章のテーマである「参照」の話を始める前に、lvalue(左辺値)rvalue(右辺値)という用語について確認しておきます。

名前から想像すると、代入式の左側に現れるのか、右側に現れるのかといった違いのように思えますが、あまり関係ありません。

まず、C++ のすべての式は、lvalue か rvalue のいずれかに分類できます。

lvalue は、変数や関数のように、名前が付いているものを示しています。rvalue は、lvalue でないものすべてです。たとえば、「100」のような定数は rvalue です。

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

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

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

int func();

int main()
{
    int a, b;

    a = 0;       // a は lvalue、0 は rvalue
    b = a;       // b は lvalue、a も lvalue

    func();      // func() の戻り値は rvalue なので、rvalue
    a = func();  // a は lvalue、func() は rvalue
}

変数a は、代入演算子の左側に登場しても右側に登場しても、a という名前を持った lvalue です。

func関数は int型の戻り値を返していますが、関数の戻り値は通常 rvalue です。そのため、func関数の呼び出し式も rvalue です。

原則として、lvalue が期待されているところでは lvalue しか使えませんし、rvalue が期待されているところでは rvalue しか使えません。たとえば、代入演算子の左辺側は(const でない)lvalue であることが期待されますから、rvalue へ代入できません。次の例を見れば、確かにできそうにないことが分かると思います。

int func();

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

    0 = a;       // 0 は rvalue なのでエラー
    3 + 5 = a;   // 3 + 5 は rvalue なのでエラー
    func() = a;  // func() は rvalue なのでエラー
    b = 10;      // b は const付きの lvalue なのでエラー
}

コンパイラが出力するエラーメッセージには、lvalue や rvalue という言葉が含まれていることがあります。この項で得た知識があれば、こういったエラーメッセージも理解しやすくなるでしょう。

参照

ポインタの代わりに、C++ では、参照(リファレンス)という機能を使うことができます。

参照はポインタとよく似ています。参照型のオブジェクトは、つねにオブジェクトや関数を参照しており、別名として機能します。

参照型の変数を定義するには、次のように記述します。

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

(非const な)参照型変数の初期化子には lvalue を指定しなければなりません。つまり、名前の付いたオブジェクトや関数を指定できます。

const修飾子を付ける場合は事情が違って、rvalue を参照できます。これについては、後で取り上げます

「型名」のところに指定する型と、初期化子の型は一致していなければなりません。たとえば、int&型の参照が、double型の変数を参照できません。

なお、参照型の変数自身も lvalue です。

参照型の変数を定義するときには、必ず初期化子が必要です。このルールがあるため、ポインタにおけるヌルポインタのような状態はなく、必ず何かを参照していることが保証されます。未初期化な状態になることもあり得ません。安全性において、ポインタよりも優秀であるといえます。

具体的には、次のように使うことができます。

#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 の値を変更しても同じことですし、どちらの値を出力してみても同じ結果を得られます。

参照によって参照先の変数を使うときに、ポインタのように「*」などの演算子を使う必要はありません。

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

struct S {
public:
    void f() {}
    int v;
};

int main()
{
    S a;
    S& r = a;

    r.f();
    r.v = 0;
}

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

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

参照の参照はできませんが、参照を参照すること自体はできます。この場合に型名の & が増えていくわけではありません。

#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 を使っても変更後の値が確認できています。


参照渡し

引数に参照を使うようにすると、関数に大きなオブジェクトを渡すときの負荷を避けられます。引数に参照を渡す形は、参照渡しと呼ばれます。

#include <iostream>

struct S {
    int v;

    // もっと巨大であるとする
};

void func(S& ref)
{
    ref.v = 100;
}

int main()
{
    S a;

    func(a);  // func関数の仮引数は参照

    std::cout << a.v << std::endl;
}

実行結果:

100

S型のオブジェクト a のコピーは作られることはなく、func関数の ref は a の別名として機能します。func関数内では ref の名前を使って、a を操作できています。

前の項で取り上げたように、参照にはヌルポインタに相当するものがないので、ヌルでないかどうかをチェックする必要もありません。安全性が向上し、余分なコードも削減されます。

func関数が、ref を使って値の書き換えを行わないのであれば、いつものように const修飾子の出番です。const 付きの参照(const参照)については、この後で詳しく取り上げますが、プログラムだけ示しておくと次のようになります。

#include <iostream>

struct S {
    int v;

    // もっと巨大であるとする
};

void func(const S& ref)
{
//    ref.v = 100;  // const参照は参照先を書き換えられない

    std::cout << ref.v << std::endl;
}

int main()
{
    S a;
    a.v = 100;

    func(a);  // func関数の仮引数は const参照
}

実行結果:

100

参照戻し

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

int& func()
{
    int n = 100;

    return n;
}

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

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

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

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

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

#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参照は、rvalue を参照できます。したがって、const参照は定数を参照できます。

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

const参照で rvalue を参照できるので、関数から rvalue を受け取る際に利用できます。

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 をコピーして作られた戻り値は、名前が付いていない rvalue です。戻り値は、「int x = func();」のようにすれば、x にコピーされますが、いずれにしても戻り値自体は消えてしまいます。

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

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

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

#include <iostream>

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

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

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

実行結果:

100

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


配列の参照

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

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


解答ページはこちら

参考リンク


更新履歴

’2018/9/14 全体的に見直し修正。
コピーに関する話題を、第17章へ移動。
章のタイトルを変更(「コピー操作と参照」–>「参照」)

’2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

’2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。

’2017/11/24 「自己代入」のサンプルプログラム内にあった誤字を修正。

’2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

≪さらに古い更新履歴を展開する≫



前の章へ (第15章 const の活用)

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

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

Programming Place Plus のトップページへ



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