C++ on MSVC講習/ポインタと参照

あらすじと概要

前回は、関数を使うことで、コード片に意味を持たせられるようになりました。今回は、C言語最難関とされるポインタと、それを分かりやすく拡張した参照を解説します。ポインタは概念として、参照は単純に使うことが多く、どちらも非常に大切な機能です。

重要語

unavailable

unavailable

cv修飾子

cv修飾子は、constこんすとvolatileぼらたいる修飾子のことです。何のことか分からないと思うので、サンプルコードを示します。
cv修飾子
#include <iostream>

int main()
{
    const int n = 0;
    volatile int m;
    const volatile int o = 3;

    m = 0; // ok, volatile修飾により、この代入は最適化でも削除されない
    m = 1; // ok, 上記同様

    // n = 1; // ng, const変数に再代入することは出来ない!

    std::cout
        << "n : " << n
        << "m : " << m
        << "o : " << o;

}

void void_f()
{
    const int n = 0; // ok 
    int m = n;       // ok, ただ単に値をコピーしているだけ
    m = 10;          // ok, 変数mは普通のintの整数なので書き換えられる
}

// この4つ、全部同じ関数の宣言。オーバーロードもしてない。
// 引数のポインタでも参照でも無い型へのcv修飾は全て完全に無視される
int f(int);
int f(const int);
int f(volatile int);
int f(const volatile int);

int f(const int a)
{
    //++a; // ng, const指定されているので、値は変更できない。ポインタでも参照でもない仮引数のconstは、
           //     オーバーロードには関係しないが、仮引数を関数の本体で使うときにはcv修飾子が適用されている
    return a;
}

int g() { return 0; }
//const int g() { return 0; } // ng, ポインタでも参照でもない返り型のcv修飾は
                              //     仮引数の場合と違い、返り型だけが違う別の関数と認識される。
                              //     しかし、返り型だけが違う関数はオーバーロード出来ないのでエラー
                              //     ただし、呼び出し元で返り値を使用する時にはcv修飾は無視される

修飾子は、型名との順番が順不同であるため、いくつかの流派が存在しています。 サンプルコードではconst intなどですが、int constなどとする流派もあります。これは宗教のようなものなので、好きな書き方で書きましょう。 volatile修飾子は、指定した変数のメモリアクセスに対して以下の2点を指定します。 olatile指定した変数への1度のメモリアクセスは、正確に1度として実行される、また、volatile指定した変数同士のメモリアクセスはコード上と対応すること。主に、ハードウェアなどのプログラム外部環境との通信するための変数に指定します。正直、volatile修飾子は通常のプログラミングでは使用しないので、覚えなくてもいいです。 普通はしませんが、cv修飾子はconst_cast演算子で付け外しをすることが出来ます。static_cast同様に、<>に型名、()の中にキャストしたい値を入れます。ただし、型名の所には、この後解説する参照やポインタを用いた型でないといけません。
const_cast
const_cast < 型名 > ( 値 )
関数の仮引数や返り値も、関係ない変数間ではcv修飾子は互いに影響しないことに従います。そのため、関数の仮引数の場合なら、コピー渡しの場合の時には、cv修飾子は無視されます。従って、コピー渡しの引数のcv修飾子だけが違う関数は、全く同一の関数として扱われます。ただし、もちろん仮引数を関数の本体で使用する時はcv修飾子の効果をが発揮されます。 変数nは、const修飾しているので初期化以外では値を変更できません。ただし、値を変更しない動作である読み取りは出来るので、出力が出来ます。 変数oは、const修飾とvolatile修飾しているので、どちらの特性も持ち合わせます。初期化以外で値は変更できないうえ、メモリアクセスはコードの通りに行われます。ただ、そもそもvolatile修飾子を普通のプログラミングでは見かけないので、 今後も出てくることはほとんどないでしょう… gは、返り値の型がポインタでも参照でもなく、cv修飾子が違う場合についてです。このような時は、関数の返り型としては、違う型だと認識されるのでした。今回は、どちらも引数を取っていないので、全く同じ引数リストになっています。ということで、オーバーロードすることは出来ないので、どちらかを消さないとエラーです。
ざっくり参照
#include <iostream>
#include <string>
#include <chrono>

int &assign_zero(int &num)
{
    num = 0;
    return num;
}
int &dangerous()
{
    int n = 0;
    return n;
}
void section1()
{
    std::cout << "section1\n";
    int n = 1;
    std::cout << n << "\n";
    assign_zero(n);
    std::cout << n << "\n";
    assign_zero(n) = 5;
    std::cout << n << "\n";

    // 危ない!!
    dangerous();

    std::cout << "\n";
}

void copy(std::string str) {}
void ref(std::string &str) {}
void section2()
{
    std::cout << "section2\n";

    // ながーい文字列
    std::string s(1ull << 30, '1');

    // 実引数をコピーして仮引数を初期化
    auto start = std::chrono::system_clock::now();
    copy(s);
    auto end = std::chrono::system_clock::now();
    std::cout << "copy : " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms\n";

    // 実引数を参照することで、実引数をコピーしない
    start = std::chrono::system_clock::now();
    ref(s);
    end = std::chrono::system_clock::now();
    std::cout << "ref  : " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms\n";

    std::cout << "\n";
}

int ret_zero() { return 0; }
int &ret_as_get(int &n) { return n; }
void lvalue_reference(int &) {}
void const_lvalue_reference(const int &) {}
void rvalue_reference(int &&) {}
void section3()
{
    std::cout << "section3\n";

    int n;          // lvlaue
    "string";       // lvalue
    ret_as_get(n);  // 返り型がlvalue referenceなので、呼び出し結果はlvalue
    0;              // rvalue
    ret_zero();     // 返り型がnon referenceなので、呼び出し結果はrvalue

    lvalue_reference(n);
    // lvalue_reference(0);    // ng, lvalue referenceにはlvalueしか束縛できない

    const_lvalue_reference(n);
    const_lvalue_reference(0); // ok, const lvalue referenceは何でも束縛できる

    // rvalue_reference(n);    // ng, rvalue referenceにはrvalueしか束縛できない
    rvalue_reference(0);

    std::cout << "\n";
}

int main()
{
    section1();
    section2();
    section3();
}

実行結果例
section1
1
0
5

section2
copy : 457ms
ref  : 0ms

section3


解説

参照は変数や関数などのエイリアス、すなわち別名を宣言することが出来る機能です。 つまり、ある変数や関数を、「参照で宣言した識別子」でもアクセスできるようになります。 参照先を決定するのは初期化の際で、参照先を参照に紐づけることを束縛すると言います。 lvalueとrvalueは、先述の通り概ね識別子を持っているかで区別することが出来ます。 lvalueは識別子を持つような式なので、識別子が持つスコープなどがライフタイムです。 一方、rvalueは識別子を持たない式なので、ライフタイムはその式が含まれる文のみです。 例外はlvalueかつプログラムの初めから終わりがライフタイムの文字列リテラルなどです。 参照は必ず初期化しなくてはならず、その際にオブジェクトを束縛します。 そして、その後は新たに束縛は出来ず、初期化時に束縛したオブジェクトを参照し続けます。 このために、参照は必ず何かしらのオブジェクトを参照することが保証されます。 参照は初期化時にオブジェクトを束縛し、その後は新たに束縛することは出来ません。 そのため、参照に対する操作は、正確に束縛しているオブジェクトへの操作になります。 lvalueを束縛できるlvalue referenceの場合、識別子を持つオブジェクトを束縛するため、 例えば、int a, &r = a;とある時に、r = 0;とすると正確にa = 0;と同じになります。

参照

...
参照
#include <iostream>

/* section 1 ***********************************************************************************************/
int rvalue() { return 0; }
const int rvalue_() { return 0; }
const int &&crvalue() { return 0; }
void section1()
{
    int lvalue = 0;         // lvalue       (変数はlvalue)
    const int clvalue = 0;  // const lvalue (const付き)
    int rvalue();           // rvalue       (返り値が参照でない関数の返り値はrvalue)
    const int rvalue_();   // rvalue        (const rvalueかと思いきや、ただのrvalue。つまり返り値intと同じ)
    const int &&crvalue();  // const rvalue  (これはrvalue referenceへのconstなので、const rvalue)

    int value0 = lvalue;              // ok
    int value1 = clvalue;             // ok
    int value2 = rvalue();            // ok
    int value3 = rvalue_();          // ok
    int value4 = crvalue();           // ok

    int &l_ref0 = lvalue;             // ok
    //int &l_ref1 = clvalue;          // ng, 1.(cv指定子が消える方へは束縛できない)
    //int &l_ref2 = rvalue();         // ng, 2.(lvalue referenceにrvalueは束縛できない)
    //int &l_ref3 = rvalue_();        // ng, 2.
    //int &l_ref4 = crvalue();        // ng, 1. + 2.

    const int &cl_ref0 = lvalue;      // ok
    const int &cl_ref1 = clvalue;     // ok
    const int &cl_ref2 = rvalue();    // ok
    const int &cl_ref3 = rvalue_();   // ok
    const int &cl_ref4 = crvalue();   // ok

    //int &&r_ref0 = lvalue;          // ng, 1.(rvalue referenceにlvalueは束縛できない)
    //int &&r_ref1 = clvalue;         // ng, 1. + 2.(cv指定子が消える方へは束縛できない)
    int &&r_ref2 = rvalue();          // ok
    int &&r_ref3 = rvalue_();         // ok
    //int &&r_ref4 = crvalue();       // ng, 2.

    //const int &&cr_ref0 = lvalue;   // ng, 1.(const rvalue referenceにlvalueは束縛できない)
    //const int &&cr_ref1 = clvalue;  // ng, 1.
    const int &&cr_ref2 = rvalue();   // ok
    const int &&cr_ref3 = rvalue_();  // ok
    const int &&cr_ref4 = crvalue();  // ok
}
/**********************************************************************************************************/


/* section 2 **********************************************************************************************/
void show_n_lref(int n, int m)
{
    std::cout << "n    : " << n << "\tlref : " << m << "\n";
}

void show_rref(int n)
{
    std::cout << "rref : " << n << "\n";
}

void section2()
{
    int n = 1, &lref = n;

    show_n_lref(n, lref);

    n = 2;

    show_n_lref(n, lref);

    lref = 3;

    show_n_lref(n, lref);


    std::cout << "\n";


    int &&rref = 42;

    show_rref(rref);

    rref = 334;

    show_rref(rref);

    std::cout << "\n";
}
/**********************************************************************************************************/

int main()
{
    section1();
    section2();
}

実行結果
n    : 1  lref : 1
n    : 2  lref : 2
n    : 3  lref : 3

rref : 42
rref : 334


参照解説

参照は変数や関数などのエイリアス、即ち別名を宣言できるようになる機能です。 参照は、必ず初期化をしなくてはならず、その時にその参照が参照する先を決定します。 そして、初期化時以外に参照の参照先を決定・変更することは一切出来ません。 なお、この初期化時に行う、参照に変数や値などを紐づけることを束縛すると言います。 N4861に示されている、式の分類の表のように、式は文法上、カテゴリを持ちます。 「値」と私達が呼ぶものは、式としてlvalueえるばりゅーxvalueえっくすばりゅーpvalueぴーばりゅーに大別されます。 また、lvalueとxvalueの一部がglvalueじーえるばりゅー、xvalueの一部とpvalueがrvalueあーばりゅーでもあります。
categories expression expression glvalue glvalue expression->glvalue rvalue rvalue expression->rvalue lvalue lvalue glvalue->lvalue xvalue xvalue glvalue->xvalue rvalue->xvalue prvalue prvalue rvalue->prvalue
Figure 1: Expression category taxonomy  [fig:basic.lval]
参照には2種類あると言いましたが、それぞれ束縛できる値カテゴリが違うのです。 まず、lvalue referenceは、その名の通りlvalueを束縛することが出来ます。 そして、rvalue referenceも、その名の通りrvalueを束縛することが出来ます。 ただし、const lavlue referenceだけはどちらも束縛することが出来るとされています。
参照にcv修飾子を付けている場合、当然、その参照を通じた操作の時に反映されます。 const修飾子なら、指定した参照から、参照先の変数を変更できないようになるし、 volatile修飾子なら、指定した参照からのメモリアクセスがコードと対応します。

まとめ

参照の種類

束縛できる値カテゴリ(+ const修飾子)で、束縛できるもの

lvalue reference

lvalue

const lvalue reference

lvalue, const lvalue, rvalue, const rvlalue

rvalue reference

rvalue

const rvalue reference

rvalue, const rvalue

サンプルコードについて1 - section1

サンプルコードの関数section1では、ここまでで説明した事が示されています。
つまり、lvalue/rvalueと、lvalue reference/rvalue referenceについて、
束縛できる関係と、cv修飾子が外れるような束縛が出来ない事が示されています。

右辺値参照の参照先、変わってない…? - section2

関数section2で、変数rrefは初め42でしたが、その後334になるのは変だと思って
42には代入出来ないのだから、参照先が変更されている?」となるかもしれません。
ですが、rvalue referenceは束縛する右辺値用に変数を用意するような働きをしているだけです。
例えば、「代入を禁止した型」のような型があった時、束縛するときは初期化なので問題なし、
しかしその後は、rvalue referenceが用意した変数への操作なので、代入出来なくなります。

サンプルコードについて2 - section2

関数section1は規則の確認でしたが、関数section2では参照の効果の確認です。
変数lrefは変数nを束縛しているので、lrefへの操作はnへの操作になります。
変数rrefについては、先述した通りなので、割愛します。
忘れがちですが、lvalue/rvalue referenceのどちらも参照それ自体はlvalueです。
もちろん、束縛している値がlvalueだろうが、rvalueだろうが、参照それ自体はlvalueです。

値カテゴリ

参照の前に解説しないといけない内容は、修飾子だけでなく値カテゴリもあります。 値カテゴリの一部を理解することで、参照を完全に理解分かった気になれるようになります。
値カテゴリ

ポインタ

参照が関数等で実際に使われる場合を示す前に、参照の元のポインタについて解説します。 ポインタはC言語でも初学者を躓かせる機能とされていますが、構文が理解しづらいだけです。 C++のメモリモデルをしっかりと認識すればそれ程難しくはありません。
ポインタ

参照、出典

参照や出典です

参照

[dcl.pre]

https://timsong-cpp.github.io/cppwp/n4861/dcl.pre

[dcl.decl]

https://timsong-cpp.github.io/cppwp/n4861/dcl.decl

[dcl.ptr]

https://timsong-cpp.github.io/cppwp/n4861/dcl.ptr

[dcl.ref]

https://timsong-cpp.github.io/cppwp/n4861/dcl.ref

[basic.lval]

https://timsong-cpp.github.io/cppwp/n4861/basic.lval

[dcl.fct]

https://timsong-cpp.github.io/cppwp/n4861/dcl.fct

cv (const および volatile) 型修飾子 - cppreference.com

https://ja.cppreference.com/w/cpp/language/cv

ほとんどのvolatileを非推奨化 - cpprefjp C++日本語リファレンス

https://cpprefjp.github.io/lang/cpp20/deprecating_volatile.html

const_cast 変換 - cppreference.com

https://ja.cppreference.com/w/cpp/language/const_cast

値カテゴリ - cppreference.com

https://ja.cppreference.com/w/cpp/language/value_category

みんなlvalueとrvalueを難しく考えすぎちゃいないかい? - Qiita

https://qiita.com/yumetodo/items/8eae5714a6cfe1c0407d

参照宣言 - cppreference.com

https://ja.cppreference.com/w/cpp/language/reference

参照初期化 - cppreference.com

https://ja.cppreference.com/w/cpp/language/reference_initialization