📢 Webサイト閉鎖と移転のお知らせ
このWebサイトは2026年9月に閉鎖いたします。
新しい記事は移転先で追加しております。(旧サイトでは記事を追加しておりません)
概要
左辺値 (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;
}