概要

左辺値 (lvalue) は、C言語から存在する基本的な概念であり、メモリ上の特定の場所を持つオブジェクトを指す。
C++では、左辺値参照 (lvalue reference) を使用して、オブジェクトへの別名を作成できる。


左辺値 (lvalue)

定義

左辺値とは、メモリ上の特定のアドレスを持つオブジェクトを指す。
名前を持ち、プログラム実行中に識別可能なエンティティである。

特徴

左辺値は、以下に示す特徴を持つ。

  • 名前を持つ変数や、参照、デリファレンス演算子(*)を使用した式等が該当する。
  • 代入演算子の左側に現れることができる。
  • &演算子を使用して、そのアドレスを取得できる。
  • プログラム実行中、一定期間メモリ上に存在し続ける。
  • 識別可能であり、複数回参照できる。


左辺値の例

 #include <iostream>
 
 class MyClass
 {
 public:
    int value;
    MyClass(int v) : value(v) {}
 };
 
 int globalVar = 100;  // グローバル変数は左辺値
 
 int main()
 {
    // 変数は左辺値
    int x = 10;
    int y = 20;
 
    // 配列要素は左辺値
    int arr[5] = {1, 2, 3, 4, 5};
    arr[2] = 30;
 
    // オブジェクトのメンバは左辺値
    MyClass obj(42);
    obj.value = 50;
 
    // ポインタのデリファレンスは左辺値
    int* ptr = &x;
    *ptr = 15;
 
    // 配列名は左辺値
    int* arrPtr = arr;
 
    // 文字列リテラルは左辺値(特殊なケース)
    const char* str = "Hello";  // "Hello"は左辺値
 
    // 関数は左辺値
    int (*funcPtr)() = globalVar;  // 関数へのポインタ
 
    // すべての左辺値はアドレスを取得できる
    int* xAddr = &x;
    int* yAddr = &y;
    int* arrElemAddr = &arr[0];
    int* objValueAddr = &(obj.value);
 
    return 0;
 }


左辺値になるもの

以下に示すものは左辺値である。

  • 変数名
  • 配列要素
  • クラスのメンバ変数
  • ポインタのデリファレンス
  • 文字列リテラル
    const char[]型
  • 左辺値参照を返す関数呼び出し
  • 前置インクリメント・デクリメント
    ++x, --x
  • 代入式
    例 : int x = y


左辺値にならないもの

以下に示すものは左辺値ではなく、右辺値である。

  • 数値リテラル
    例 : 42, 3.14等
  • 算術演算の結果
    x + y
  • 非参照を返す関数呼び出し
  • 後置インクリメント・デクリメント
    x++, x--
  • 一時オブジェクト



左辺値参照 (lvalue reference)

定義

左辺値参照は、既存のオブジェクトへの別名 (エイリアス) である。
C++98から存在する従来の参照型であり、型名& という記法で表現される。

基本的な構文

 int x = 10;
 int& ref = x;         // 左辺値参照の宣言と初期化
 
 const int& cref = x;  // const左辺値参照


左辺値参照の特性

左辺値参照は、以下に示す特性を持つ。

  • 必ず初期化時に束縛する必要がある。
  • 1度束縛すると、別のオブジェクトに再束縛できない。
  • 参照自体はメモリを持たない。(コンパイラによる最適化の対象)
  • 参照を通じた操作は、束縛したオブジェクトへの操作と同じである。
  • 左辺値参照は、左辺値のみを束縛できる。
  • const左辺値参照は、左辺値と右辺値の両方を束縛できる。


使用例

 #include <iostream>
 
 int main()
 {
    int x = 10;
    int& ref = x;  // 左辺値参照の作成
 
    std::cout << "x: " << x << std::endl;      // 10
    std::cout << "ref: " << ref << std::endl;  // 10
 
    // 参照を通じた値の変更
    ref = 20;
    std::cout << "x after ref = 20: " << x << std::endl;  // 20
 
    // 変数を通じた値の変更
    x = 30;
    std::cout << "ref after x = 30: " << ref << std::endl;  // 30
 
    // 同じアドレスを指している
    std::cout << "Address of x: " << &x << std::endl;
    std::cout << "Address of ref: " << &ref << std::endl;
 
    return 0;
 }


左辺値参照の束縛

 #include <iostream>
 
 int main()
 {
    int x = 10;
    int y = 20;
 
    // 左辺値参照は左辺値を束縛できる
    int& ref1 = x;  // OK
 
    // 左辺値参照は右辺値を束縛できない
    // int& ref2 = 10;     // コンパイルエラー
    // int& ref3 = x + y;  // コンパイルエラー
 
    // 参照の「代入」は、束縛先の値を変更する
    int& ref2 = y;
    ref1 = ref2;  // xの値がyの値になる(ref1がref2を指すわけではない)
 
    std::cout << "x: " << x << std::endl;  // 20
    std::cout << "y: " << y << std::endl;  // 20
 
    // 参照を再束縛することはできない
    // ref1はxへの別名であり続ける
 
    return 0;
 }


const左辺値参照

const左辺値参照は、特別な性質を持つ。

 #include <iostream>
 #include <string>
 
 void printValue(const int& value)
 {
    std::cout << "Value: " << value << std::endl;
 }
 
 void printString(const std::string& str)
 {
    std::cout << "String: " << str << std::endl;
 }
 
 int main()
 {
    int x = 10;
 
    // const左辺値参照は左辺値を束縛できる
    const int& cref1 = x;
 
    // const左辺値参照は右辺値を束縛できる
    const int& cref2 = 20;      // OK
    const int& cref3 = x + 5;   // OK
 
    // const左辺値参照を通じた値の変更はできない
    // cref1 = 30;  // コンパイルエラー
 
    // 元の変数を通じた値の変更は可能
    x = 30;
    std::cout << "cref1 after x = 30: " << cref1 << std::endl;  // 30
 
    // 関数の引数としてのconst左辺値参照
    printValue(x);              // 左辺値を渡す
    printValue(42);             // 右辺値を渡す
    printValue(x + 10);         // 式の結果(右辺値)を渡す
 
    std::string str = "Hello";
    printString(str);           // 左辺値を渡す
    printString("World");       // 右辺値を渡す
 
    return 0;
 }


一時オブジェクトの寿命延長

const左辺値参照に束縛された一時オブジェクトは、参照のスコープが終了するまで寿命が延長される。

 #include <iostream>
 #include <string>
 
 class TempObject
 {
 public:
    TempObject() { std::cout << "TempObject created" << std::endl; }
    ~TempObject() { std::cout << "TempObject destroyed" << std::endl; }
 
    void use() const { std::cout << "Using TempObject" << std::endl; }
 };
 
 TempObject createObject()
 {
    return TempObject();
 }
 
 int main()
 {
    std::cout << "--- Without reference ---" << std::endl;
    createObject().use();  // 一時オブジェクトはすぐに破棄される
 
    std::cout << "\n--- With const lvalue reference ---" << std::endl;
    const TempObject& ref = createObject();  // 寿命が延長される
    ref.use();
    std::cout << "Still in scope" << std::endl;
    // スコープ終了時に破棄される
 
    return 0;
 }



メソッドの引数としての左辺値参照

参照渡しによる効率化

大きなオブジェクトを関数に渡す時、左辺値参照を使用することでコピーを避けられる。

 #include <iostream>
 #include <vector>
 
 // 値渡し : コピーが発生する
 void processByValue(std::vector<int> vec)
 {
    std::cout << "Vector size: " << vec.size() << std::endl;
 }
 
 // 左辺値参照渡し : コピーが発生しない
 void processByReference(std::vector<int>& vec)
 {
    std::cout << "Vector size: " << vec.size() << std::endl;
 }
 
 // const左辺値参照渡し : コピーが発生せず、変更もできない
 void processByConstReference(const std::vector<int>& vec)
 {
    std::cout << "Vector size: " << vec.size() << std::endl;
 }
 
 int main()
 {
    std::vector<int> largeVec(1000000, 42);
 
    processByValue(largeVec);          // コピーが発生 (遅い)
    processByReference(largeVec);      // コピーなし (速い)
    processByConstReference(largeVec); // コピーなし(速い)、変更不可
 
    return 0;
 }


入出力パラメータ

左辺値参照を使用して、メソッド内でオブジェクトを変更できる。

 #include <iostream>
 #include <string>
 
 // 出力パラメータ
 void split(const std::string& fullName, std::string& firstName, std::string& lastName)
 {
    size_t pos = fullName.find(' ');
    if (pos != std::string::npos)
    {
       firstName = fullName.substr(0, pos);
       lastName = fullName.substr(pos + 1);
    }
 }
 
 // 入出力パラメータ
 void appendSuffix(std::string& str, const std::string& suffix)
 {
    str += suffix;
 }
 
 int main()
 {
    std::string fullName = "John Doe";
    std::string first, last;
 
    split(fullName, first, last);
    std::cout << "First: " << first << ", Last: " << last << std::endl;
 
    std::string message = "Hello";
    appendSuffix(message, " World!");
    std::cout << message << std::endl;  // Hello World!
 
    return 0;
 }


関数の戻り値としての左辺値参照

 #include <iostream>
 #include <vector>
 
 class Container
 {
 private:
    std::vector<int> data;
 
 public:
    Container() : data{1, 2, 3, 4, 5} {}
 
    // 左辺値参照を返す
    int& at(size_t index)
    {
       return data.at(index);
    }
 
    // const左辺値参照を返す
    const int& at(size_t index) const
    {
       return data.at(index);
    }
 };
 
 int main()
 {
    Container c;
 
    // 左辺値参照を返すため、直接変更可能
    c.at(0) = 10;
    std::cout << c.at(0) << std::endl;  // 10
 
    // 連鎖代入も可能
    c.at(1) = c.at(2) = 20;
    std::cout << c.at(1) << ", " << c.at(2) << std::endl;  // 20, 20
 
    // const版の使用
    const Container constC;
    int value = constC.at(0);  // OK
    // constC.at(0) = 100;     // コンパイルエラー
 
    return 0;
 }



左辺値参照の実用例

範囲for文での使用

 #include <iostream>
 #include <vector>
 
 int main()
 {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
 
    // コピーが発生する
    for (auto num : numbers)
    {
       num *= 2;  // ローカルコピーを変更 (元のベクタは変わらない)
    }
 
    // 参照を使用 (元のベクタを変更)
    for (auto& num : numbers)
    {
       num *= 2;  // 元のベクタの要素を変更
    }
    
    // const参照を使用 (変更不可、コピーなし)
    for (const auto& num : numbers)
    {
       std::cout << num << " ";  // 読み取り専用
    }
    std::cout << std::endl;
 
    return 0;
 }


メンバ変数としての左辺値参照

 #include <iostream>
 #include <string>
 
 class Logger
 {
 private:
    std::ostream& output;  // 左辺値参照をメンバに持つ
 
 public:
    // コンストラクタで参照を初期化(必須)
    Logger(std::ostream& out) : output(out) {}
 
    void log(const std::string& message)
    {
       output << "[LOG] " << message << std::endl;
    }
 };
 
 int main()
 {
    Logger consoleLogger(std::cout);
    consoleLogger.log("This goes to console");
 
    std::ofstream file("log.txt");
    Logger fileLogger(file);
    fileLogger.log("This goes to file");
 
    return 0;
 }


スワップ操作

 #include <iostream>
 #include <utility>
 
 // 左辺値参照を使用したスワップ
 void swap(int& a, int& b)
 {
    int temp = a;
    a = b;
    b = temp;
 }
 
 // より効率的なムーブを使用したスワップ
 template<typename T>
 void swapMove(T& a, T& b)
 {
    T temp = std::move(a);
    a = std::move(b);
    b = std::move(temp);
 }
 
 int main()
 {
    int x = 10, y = 20;
 
    std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
    swap(x, y);
    std::cout << "After swap: x = " << x << ", y = " << y << std::endl;
 
    std::string s1 = "Hello", s2 = "World";
 
    std::cout << "Before swap: s1 = " << s1 << ", s2 = " << s2 << std::endl;
    swapMove(s1, s2);
    std::cout << "After swap: s1 = " << s1 << ", s2 = " << s2 << std::endl;
 
    return 0;
 }



その他

ダングリング参照

参照が指すオブジェクトのスコープが終了すると、ダングリング参照(dangling reference)が発生する。

 #include <iostream>
 
 int& getDanglingReference()
 {
    int local = 42;
    return local;  // 危険: ローカル変数への参照を返す
 }
 
 int main()
 {
    // int& ref = getDanglingReference();  // 未定義動作
    // std::cout << ref << std::endl;      // 危険
 
    return 0;
 }


参照の初期化は必須

 int main()
 {
    // int& ref;  // コンパイルエラー: 参照は初期化が必須
 
    int x = 10;
    int& ref = x;  // OK
 
    return 0;
 }


参照の配列は作成できない

 int main()
 {
    int x = 10, y = 20, z = 30;
 
    // int& refs[3] = {x, y, z};  // コンパイルエラー: 参照の配列は不可
 
    // ポインタの配列は可能
    int* ptrs[3] = {&x, &y, &z};  // OK
 
    return 0;
 }


nullptrや未初期化の参照は存在しない

 int main()
 {
    // 参照はnullにできない
    // int& ref = nullptr;  // コンパイルエラー
 
    // ポインタとの違い
    int* ptr = nullptr;  // OK: ポインタはnullにできる
 
    return 0;
 }



左辺値参照とポインタの比較

左辺値参照とポインタは似ているが、重要な違いがある。

特性 左辺値参照 ポインタ
初期化 必須 任意
null値 不可 可能
再代入 不可 (束縛先の値を変更) 可能 (別のアドレスを指す)
構文 シンプル (オブジェクトと同じ) *演算子が必要
オーバーヘッド 通常なし (最適化される) あり (間接参照)
配列 作成不可 作成可能
ポインタ演算 不可 可能


 #include <iostream>
 
 int main()
 {
    int x = 10;
    int y = 20;
 
    // 左辺値参照
    int& ref = x;
    ref = y;  // xの値が20になる (refがyを指すわけではない)
    std::cout << "x: " << x << std::endl;  // 20
 
    // ポインタ
    int* ptr = &x;
    ptr = &y;  // ptrがyを指すようになる
    std::cout << "x: " << x << std::endl;  // 20(変わらない)
 
    return 0;
 }



最適化とインライン展開

コンパイラは、左辺値参照を最適化することが多い。
特に、小さな関数では、参照がインライン展開され、実質的なオーバーヘッドが無くなる。

 inline int& getMax(int& a, int& b)
 {
    return (a > b) ? a : b;
 }
 
 int main()
 {
    int x = 10, y = 20;
 
    // インライン展開により、効率的なコードが生成される
    getMax(x, y) = 30;  // yが30になる
 
    return 0;
 }