概要
C++ 11以降、右辺値 (rvalue) の概念が拡張されて、右辺値参照 (rvalue reference) が導入された。
これにより、ムーブセマンティクスが可能になり、不要なコピーを避けることでパフォーマンスが向上する。
右辺値 (rvalue)
定義
右辺値とは、一時的な値やメモリ上の特定のアドレスを持たないオブジェクトを指す。
通常、式の評価後に破棄される一時的な値である。
特徴
右辺値は、以下に示す特徴を持つ。
- リテラル、一時オブジェクト、非参照の戻り値を持つ関数呼び出し等が該当する。
- 代入演算子の右側にのみ現れる。
- &演算子を使用してアドレスを取得することはできない。
- 式の評価後、すぐに破棄される。
C++ 11以降の値カテゴリ
C++ 11以降、右辺値は2つのカテゴリに細分化された。
- prvalue (pure rvalue : 純粋な右辺値)
- リテラル値
- 例 : 42, 3.14, "hello"
- 非参照を返す関数やオペレータの戻り値
- 例 : x + y
- ラムダ式
- 後置インクリメント・デクリメント
- 例 : x++, x--
- リテラル値
- xvalue (eXpiring value : 期限切れ値)
- std::moveの結果
- 右辺値参照を返す関数の戻り値
- オブジェクトの寿命が終わりに近づいている値
使用例
#include <iostream>
class MyClass
{
public:
MyClass() { std::cout << "Default constructor" << std::endl; }
~MyClass() { std::cout << "Destructor" << std::endl; }
};
MyClass createObject()
{
return MyClass(); // 戻り値は右辺値(prvalue)
}
int getValue()
{
return 20;
}
int main()
{
// リテラルは右辺値
int x = 10; // 10は右辺値、xは左辺値
// 式の結果は右辺値
int y = x + 5; // x + 5は右辺値
// 関数の戻り値は右辺値
int z = getValue(); // getValue()の戻り値は右辺値
// 一時オブジェクトは右辺値
MyClass obj = createObject();
// アドレス取得の試み(エラー)
// int* ptr = &(x + 5); // コンパイルエラー: 右辺値のアドレスは取得できない
// int* ptr2 = &10; // コンパイルエラー: リテラルのアドレスは取得できない
return 0;
}
右辺値を左辺に置くことはできない
右辺値は左辺値に変換できないため、以下はコンパイルエラーとなる。
int main()
{
int x = 5;
// コンパイルエラー: 式の結果(右辺値)に代入はできない
(x + 1) = 9;
// コンパイルエラー: リテラル(右辺値)に代入はできない
10 = x;
// コンパイルエラー: 関数の戻り値(右辺値)に代入はできない
getValue() = 100;
}
右辺値参照 (rvalue reference)
定義
右辺値参照は、C++ 11で導入された新しい参照型であり、右辺値を束縛することができる。
型名&& という記法で表現される。
基本的な構文
int&& rref = 10; // 右辺値参照の宣言と初期化
std::string&& str_ref = std::string("hello"); // 一時オブジェクトへの右辺値参照
右辺値参照の特性
右辺値参照は、以下に示す特性を持つ。
- 右辺値参照は型である。
- 右辺値参照自体は左辺値である。
- 右辺値参照は右辺値のみを束縛できる。
- 右辺値参照により、一時オブジェクトの寿命が延長される。
右辺値参照の使用例
#include <iostream>
#include <string>
int getValue()
{
return 42;
}
int main()
{
// 基本的な右辺値参照
int&& rref1 = 10; // リテラルを束縛
int&& rref2 = getValue(); // 関数の戻り値を束縛
int&& rref3 = 5 + 3; // 式の結果を束縛
std::cout << "rref1: " << rref1 << std::endl; // 10
std::cout << "rref2: " << rref2 << std::endl; // 42
std::cout << "rref3: " << rref3 << std::endl; // 8
// 右辺値参照自体は左辺値なので、変更可能
rref1 = 20;
std::cout << "rref1 after modification: " << rref1 << std::endl; // 20
// 右辺値参照同士の代入
rref2 = rref1; // rref1は左辺値なので、値がコピーされる
std::cout << "rref2 after assignment: " << rref2 << std::endl; // 20
// 左辺値を右辺値参照に束縛することはできない
int x = 100;
// int&& rref4 = x; // コンパイルエラー
// std::moveを使えば可能
int&& rref5 = std::move(x);
std::cout << "rref5: " << rref5 << std::endl; // 100
return 0;
}
寿命の延長
右辺値参照に束縛された一時オブジェクトは、参照のスコープが終了するまで寿命が延長される。
#include <iostream>
#include <string>
class Resource
{
public:
Resource() { std::cout << "Resource created" << std::endl; }
~Resource() { std::cout << "Resource destroyed" << std::endl; }
void use() { std::cout << "Using resource" << std::endl; }
};
Resource createResource()
{
return Resource();
}
int main()
{
std::cout << "--- Without rvalue reference ---" << std::endl;
createResource().use(); // 一時オブジェクトはすぐに破棄される
std::cout << "\n--- With rvalue reference ---" << std::endl;
Resource&& rref = createResource(); // 寿命が延長される
rref.use();
std::cout << "Still in scope" << std::endl;
// スコープ終了時に破棄される
return 0;
}
関数のオーバーロード
右辺値参照を使用することにより、左辺値と右辺値で異なる処理を定義することができる。
#include <iostream>
#include <string>
// 左辺値参照バージョン
void process(std::string& str)
{
std::cout << "Processing lvalue: " << str << std::endl;
}
// 右辺値参照バージョン
void process(std::string&& str)
{
std::cout << "Processing rvalue: " << str << std::endl;
}
int main()
{
std::string lvalue = "left";
process(lvalue); // 左辺値参照バージョンが呼ばれる
process(std::string("right")); // 右辺値参照バージョンが呼ばれる
process("temporary"); // 右辺値参照バージョンが呼ばれる
process(std::move(lvalue)); // 右辺値参照バージョンが呼ばれる
return 0;
}
ムーブセマンティクス
ムーブセマンティクスとは
ムーブセマンティクスは、右辺値参照を利用した効率的なリソース転送の仕組みである。
コピーではなく、リソースの所有権を移動させることで、パフォーマンスを向上させる。
コピーとムーブの比較
#include <iostream>
#include <vector>
#include <chrono>
class LargeData
{
private:
std::vector<int> data;
public:
// デフォルトコンストラクタ
LargeData(size_t size = 1000000) : data(size, 42)
{
std::cout << "Constructor: created " << size << " elements" << std::endl;
}
// コピーコンストラクタ
LargeData(const LargeData& other) : data(other.data)
{
std::cout << "Copy constructor: copied " << data.size() << " elements" << std::endl;
}
// ムーブコンストラクタ
LargeData(LargeData&& other) noexcept : data(std::move(other.data))
{
std::cout << "Move constructor: moved " << data.size() << " elements" << std::endl;
}
size_t size() const { return data.size(); }
};
int main()
{
std::cout << "--- Copy operation ---" << std::endl;
LargeData obj1;
LargeData obj2 = obj1; // コピーコンストラクタが呼ばれる
std::cout << "\n--- Move operation ---" << std::endl;
LargeData obj3;
LargeData obj4 = std::move(obj3); // ムーブコンストラクタが呼ばれる
std::cout << "obj3 size after move: " << obj3.size() << std::endl; // 0
std::cout << "obj4 size after move: " << obj4.size() << std::endl; // 1000000
return 0;
}
ムーブコンストラクタ / ムーブ代入演算子
#include <iostream>
#include <cstring>
class MyString
{
private:
char* data;
size_t length;
public:
// コンストラクタ
MyString(const char* str = "")
{
length = std::strlen(str);
data = new char[length + 1];
std::strcpy(data, str);
std::cout << "Constructor: " << data << std::endl;
}
// デストラクタ
~MyString()
{
std::cout << "Destructor: " << (data ? data : "null") << std::endl;
delete[] data;
}
// コピーコンストラクタ
MyString(const MyString& other)
{
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
std::cout << "Copy constructor: " << data << std::endl;
}
// ムーブコンストラクタ
MyString(MyString&& other) noexcept
{
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
std::cout << "Move constructor: " << (data ? data : "null") << std::endl;
}
// コピー代入演算子
MyString& operator=(const MyString& other)
{
if (this != &other)
{
delete[] data;
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
std::cout << "Copy assignment: " << data << std::endl;
}
return *this;
}
// ムーブ代入演算子
MyString& operator=(MyString&& other) noexcept
{
if (this != &other)
{
delete[] data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
std::cout << "Move assignment: " << (data ? data : "null") << std::endl;
}
return *this;
}
const char* c_str() const { return data ? data : ""; }
};
int main()
{
MyString s1("Hello");
MyString s2("World");
std::cout << "\n--- Copy ---" << std::endl;
MyString s3 = s1; // コピーコンストラクタ
std::cout << "\n--- Move ---" << std::endl;
MyString s4 = std::move(s2); // ムーブコンストラクタ
std::cout << "\n--- Copy assignment ---" << std::endl;
s4 = s1; // コピー代入演算子
std::cout << "\n--- Move assignment ---" << std::endl;
s4 = std::move(s3); // ムーブ代入演算子
std::cout << "\n--- End of main ---" << std::endl;
return 0;
}
std::moveの詳細
std::move は左辺値を右辺値にキャストする関数テンプレートである。
実際にデータを移動するわけではなく、ムーブコンストラクタやムーブ代入演算子の呼び出しを可能にする。
#include <iostream>
#include <utility>
#include <vector>
int main()
{
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::cout << "v1 size before move: " << v1.size() << std::endl; // 5
// std::moveは左辺値を右辺値にキャストする
std::vector<int> v2 = std::move(v1);
std::cout << "v1 size after move: " << v1.size() << std::endl; // 0
std::cout << "v2 size after move: " << v2.size() << std::endl; // 5
// v1は有効だが不定な状態
// v1を使用する前に再初期化する必要がある
v1 = {10, 20, 30};
std::cout << "v1 size after reinitialization: " << v1.size() << std::endl; // 3
return 0;
}
ムーブセマンティクスの注意
ムーブ後のオブジェクトは有効だが不定な状態となる。
- ムーブ後のオブジェクトに対する操作は未定義動作となる可能性がある。
- ムーブ後のオブジェクトは、再代入するか破棄するのが安全である。
noexceptキーワードをムーブコンストラクタに付加することが推奨される。- ムーブコンストラクタ内で例外が発生しないことを保証する必要がある。
また、C++ 11以降では、以下に示す5つの特殊メンバ関数を定義することが推奨される。
- デストラクタ
- コピーコンストラクタ
- コピー代入演算子
- ムーブコンストラクタ
- ムーブ代入演算子
これらのいずれかのカスタム定義を記述する場合、他のものも明示的に定義、または、デフォルト、削除すべきである。
class Resource
{
public:
// デストラクタ
~Resource();
// コピー操作
Resource(const Resource& other);
Resource& operator=(const Resource& other);
// ムーブ操作
Resource(Resource&& other) noexcept;
Resource& operator=(Resource&& other) noexcept;
};
サンプルコード
コンテナへの効率的な追加
#include <iostream>
#include <vector>
#include <string>
class Widget
{
private:
std::string name;
std::vector<int> data;
public:
Widget(const std::string& n, size_t size) : name(n), data(size, 0)
{
std::cout << "Constructor: " << name << std::endl;
}
Widget(const Widget& other) : name(other.name), data(other.data)
{
std::cout << "Copy constructor: " << name << std::endl;
}
Widget(Widget&& other) noexcept : name(std::move(other.name)), data(std::move(other.data))
{
std::cout << "Move constructor: " << name << std::endl;
}
};
int main()
{
std::vector<Widget> widgets;
std::cout << "--- Using push_back with lvalue ---" << std::endl;
Widget w1("Widget1", 1000);
widgets.push_back(w1); // コピーが発生
std::cout << "\n--- Using push_back with rvalue ---" << std::endl;
widgets.push_back(Widget("Widget2", 1000)); // ムーブが発生
std::cout << "\n--- Using push_back with std::move ---" << std::endl;
Widget w3("Widget3", 1000);
widgets.push_back(std::move(w3)); // ムーブが発生
std::cout << "\n--- Using emplace_back ---" << std::endl;
widgets.emplace_back("Widget4", 1000); // 直接構築、最も効率的
return 0;
}
関数からのオブジェクト返却
#include <iostream>
#include <vector>
// 戻り値最適化 (RVO) により、通常はコピーもムーブも発生しない
std::vector<int> createVector()
{
return std::vector<int>(1000, 42);
}
// 名前付き戻り値最適化 (NRVO) の例
std::vector<int> createNamedVector()
{
std::vector<int> result(1000, 42);
return result; // NRVOが適用される可能性がある
}
int main()
{
auto v1 = createVector(); // RVO
auto v2 = createNamedVector(); // NRVO
return 0;
}
その他
右辺値参照は常に右辺値ではない
右辺値参照型の変数自体は左辺値である。
void func(int&& rref)
{
// rrefは右辺値参照型だが、左辺値である
int* ptr = &rref; // アドレスを取得できる
}
std::moveは実データを移動しない
std::moveは単なるキャストであり、データの移動は行わない。
実際のムーブは、ムーブコンストラクタやムーブ代入演算子で行われる。
ムーブ後のオブジェクト使用
#include <iostream>
#include <string>
int main()
{
std::string str1 = "Hello";
std::string str2 = std::move(str1);
// 危険: str1は有効だが不定な状態
// std::cout << str1 << std::endl; // 未定義動作の可能性
// 安全 : 再代入
str1 = "World";
std::cout << str1 << std::endl; // OK
return 0;
}
パフォーマンスへの影響
ムーブセマンティクスは、以下に示すような状況で大きなパフォーマンス向上をもたらす。
- 大きなコンテナや文字列の操作
- 関数からのオブジェクト返却
- コンテナへのオブジェクト追加
- スワップ操作
- 一時オブジェクトの処理
ただし、小さなオブジェクト (int、double等のプリミティブ型) では効果は限定的である。