1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

結局「右辺値参照」って何なんだ

Last updated at Posted at 2025-10-03

概要

どのサイトを見ても、C++の「右辺値参照」がわからないので、
何番煎じかわかりませんが、自分で書くことにします。

※筆者も初学者のため、正確性に欠けるところがあると思います。
 修正いたしますので、ぜひお教えください。皆様のご指摘に感謝します。

要するに

無駄なコピーを行わなくすることで、パフォーマンスを上げるために存在する機能。

この記事で解説すること

  • 右辺値、左辺値ってそもそも何なの
  • 右辺値参照(&&)っていつ使うの
  • std::move()って要するに何

先人の記事

一番わかりやすかった記事はこちら↓です。

上記サイトより引用します。

右辺値は名前が悪い。 一時オブジェクト あるいはアドレスを持たないものと考えると良い
std::moveは名前が悪い。移動ではなく右辺値へのキャスト演算子である
関数の仮引数で右辺値参照(hoge&&) で受けても左辺値(hoge&)であるので右辺値にするには std::move()でキャストする

ほとんどこれに集約されます。
では、具体例を交えて解説していきます。

解説

右辺値、左辺値ってそもそも何なの

この記事にたどり着く前に何度もお調べかと思いますが、こちらでも解説します。

まず大前提として、「右辺値」「左辺値」は名前が悪いです
もう少しいい名前を付けてあげればよかったのに。

「右辺値」「左辺値」などと称されるものは、「値」ではなく、
より正確に言えば表現とするのが正しいです。
さらに正確に言うと、この「表現(expression)」は、
"lvalue"と"prvalue"、そしてその中間の"xvalue"に分かれていて、
"lvalue"と"xvalue"をまとめて"glvalue"と呼び、
"prvalue"と"xvalue"を包含した概念が"rvalue"で……
……となりますが、ここを深追いすると初学者には厳しくなってきますので、
一度この記事では差し置きます。

「右辺値参照」を理解するためには、
いったん「左辺値(lvalue)」と「右辺値(rvalue)」が存在する、ということのみ
把握しておけば大丈夫です。

そして、何度も言いますが、これらは名前が悪いです。

というわけで、本記事では右辺値を「右辺値(保存されない値)」と表記します。
また左辺値を「左辺値(保存された値)」と表記します。
細かくは後でわかってくると思いますが、まずこの前提で進めていきますのでご了承ください。

右辺値、左辺値とは

例えば、簡単な以下のプログラムを考えましょう。

int x = (3 + 2) * 10;         // ...(1)
std::cout << x << std::endl;  // ...(2)

(3 + 2) * 10を計算して表示するプログラムです。
実行すれば、50と表示されますね。

ここで、(1)の行について考えましょう。
(1)では、xという変数に、計算結果の値(ここでは50)が代入されることになります。
この値は、メモリー上に保存されているので、(スコープ内であれば)以降もxという名前で使えます。
実際に、(2)の行で参照されて、50と表示されますね。
つまり、xはint型の「左辺値(保存された値)」です。

一方で、(1)の行の210はプログラム上で二度と参照されることはありません。
例えば、(3 + 2)の答えである5は、メモリーに保存されないわけです。
つまり、これらは「右辺値(保存されない値)」です。

別の例を考えましょう。

int add(int a, int b){
  return a + b;
}
int mul(int a, int b){
  return a * b;
}
int main(){
  int x = 3;
  int y = 4;
  std::cout << add(x, y) << std::endl;
  std::cout << mul(x, y) << std::endl;
}

先ほどと同様に、xyは左辺値(保存された値)ですが、
一方でadd(x, y)(値としては7)はどうでしょうか。
この7という値は、以降の行でもう一度使いたいとなったとしても、
(変数に代入されていないので)使うことができません。
もう一度計算するしかないですね。
つまり、add(x, y)は右辺値(保存されない値)です。

大まかにわかってきたでしょうか。では次へ行きます。

右辺値は、通常の変数に代入することで左辺値にできる

以下の例を考えましょう。

int add(int a, int b){
  return a + b;
}
int main(){
  std::cout << add(3, 4) << std::endl;  // ...(1)
  int z = add(5, 6);                    // ...(2)
  std::cout << z << std::endl;          // ...(3)
}

(1)は右辺値(保存されない値)であるadd(3, 4)をそのまま表示します。(値は7)
この値は二度と使用されないので、すぐ消えてしまいます。
一方で、(2)の行では、右辺値(保存されない値)であるadd(5, 6)を変数zに代入しています。
そのおかげで、左辺値(保存された値)となり、(3)の行で再度使用できています。
右辺値(保存されない値)は、(通常の)変数に代入することで左辺値(保存された値)にできるわけですね。

当たり前のことを言っているだけでした。しかし、重要なことです。

右辺値は、参照型の変数には代入できない

以下の例を考えましょう。

int add(int a, int b){
  return a + b;
}
int main(){
  int x = add(2, 3);                    // ...(1)
  int& y = x;                           // ...(2)
  std::cout << y << std::endl;          // ...(3)
}

(2)では、参照型の変数yに、左辺値(保存された値)のxを代入しています。
当然コンパイルは通り、値5が表示されます。

一方で、以下の例はどうでしょう。

int add(int a, int b){
  return a + b;
}
int main(){
  int& y = add(2, 3);                   // ...(1)
  std::cout << y << std::endl;          // ...(2)
}

この例は、コンパイルエラーになります。
アドレスが割り当てられていない右辺値(保存されない値)を、
参照型の変数yに代入しようとしたために、エラーになるわけです。
つまり、右辺値(保存されない値)は参照型の変数に代入できないのです。

ここまでわかれば、右辺値と左辺値は理解できたと言えるでしょう。

右辺値参照(&&)っていつ使うの

右辺値を変数に代入する際、コピーが発生する

以下の例を考えます。

void triple(std::vector<int>& vec){
  for(int& elem : vec){
    elem *= 3;
  }
}
std::vector<int> same_vec(std::vector<int>& vec){ // ...(1)
  return vec;
}
int main(){
  std::vector<int> x = {1, 2, 3, 4, 5};
  std::vector<int> y = same_vec(x);               // ...(2)
  triple(x);
  for(int elem : y) {
    std::cout << elem << ", ";
  }
  std::cout << std::endl;
}

何と表示されるでしょうか。

答えは、1, 2, 3, 4, 5, です。
あれ、(1)で参照を渡しているはずでは……と思った方は、勘が鋭いですね。
実は、(2)の時点で、変数のコピーが発生しています。
そのため、xyの指すものは異なってくるわけです。

右辺値参照で無駄なコピーを防ぐ

以下の例を考えます。

void triple(std::vector<int>& vec){
  for(int& elem : vec){
    elem *= 3;
  }
}
std::vector<int> get_vec(int n){
  std::vector<int> vec = {n, n, n, n, n};
  triple(vec);
  return vec;
}
int main(){
  std::vector<int> z = get_vec(2);           // ...(1)
  for(int elem : z) {
    std::cout << elem << ", ";
  }
  std::cout << std::endl;
}

6, 6, 6, 6, 6が表示されます。
先ほどの例を考えると、(1)で、不必要なコピーが発生していることがお判りでしょうか。
この程度の大きさであれば問題ないですが、
これがもし巨大な構造体だった場合、コピーコストがかかります。リソースが無駄ですね。

そこで、右辺値参照(&&)が役に立ちます。
(1)の行を、以下のように書きかえましょう。

void triple(std::vector<int>& vec){
  for(int& elem : vec){
    elem *= 3;
  }
}
std::vector<int> get_vec(int n){
  std::vector<int> vec = {n, n, n, n, n};
  triple(vec);
  return vec;
}
int main(){
  std::vector<int>&& z = get_vec(2);         // ...(1)
  for(int elem : z) {
    std::cout << elem << ", ";
  }
  std::cout << std::endl;
}

結果は全く同じです。しかし、(1)で右辺値参照(&&)を行っています。
これで、get_vec(2)で得た右辺値(保存されない値)を、
コピーせず、受け取ることができるのです。
これで、リソースが軽くなるというわけです!

std::move()って要するに何

次は、ムーブ(std::move)について解説します。

ムーブとは、右辺値(保存されない値)へのキャストと考えてよいです。

例を見ましょう。

ムーブの例

  std::vector<int> x = {1, 2, 3};
  std::vector<int> y = std::move(x);           // ...(1)

(1)で、ムーブが行われています。
これは、xを右辺値(保存されない値)にキャスト」
つまり xはもう使わないことを表します。
この行以降のxの挙動は不定です。xは使わないようにしましょう。

しかし、なぜこのような機能があるのでしょうか。

ムーブの役割

ムーブは直訳すると「移動」ですが、これも若干名前が悪いですね。
その実態は、変数への代入後、コピー元を右辺値(保存されない値)にするという動作です。
代入 & コピー元の右辺値(保存されない値)へのキャストですね。

ムーブが威力を発揮するのは、以下のような場合です。

std::vector<int> append(std::vector<int>& vec){            // ...(A)
  vec.push_back(10);
  vec.push_back(20);
  vec.push_back(30);
  std::cout << "copy" << std::endl;
  return vec;
}

std::vector<int> append(std::vector<int>&& vec){           // ...(B)
  vec.push_back(10);
  vec.push_back(20);
  vec.push_back(30);
  std::cout << "move" << std::endl;
  return std::move(vec);                                   // ...(★)
}

int main(){
  std::vector<int> tmp = std::vector<int>{1, 2, 3};
  std::vector<int> x = append(tmp);                        // ...(1)

  for(int elem: x){
    std::cout << elem << ", ";
  }
  std::cout << std::endl;

  std::vector<int> y = append(std::vector<int>{1, 2, 3});  // ...(2)
  for(int elem: y){
    std::cout << elem << ", ";
  }
  std::cout << std::endl;
}

実行結果は以下のようになります。

copy
1, 2, 3, 10, 20, 30,
move
1, 2, 3, 10, 20, 30,

関数append()が2つありますが、引数の型が、ただの参照と右辺値参照で違います。
つまり、(A)と(B)の2つが両方存在している、オーバーロードの状態です。

(1)は、左辺値(保存された値)tmpappend()を呼び出しているので、(A)のほうが呼ばれます。そのため、"copy"と表示されます。
(逆に、(B)のみしか定義されていなかったなら、エラーになります。左辺値は右辺値参照型に代入できないからです。)

(2)は、右辺値(保存されない値)std::vector<int>{1, 2, 3}append()を呼び出しているので、(B)のほうが呼ばれます。そのため、"move"と表示されます。
(逆に、(A)のみしか定義されていなかったなら、エラーになります。上記にあるように、参照型に右辺値は代入できません。)

ここで大事なのは(★)の行です。
std::move()を利用することで、
関数(B)内のvecが右辺値(保存されない値)として返されます。
すなわち、(2)の行で無駄なコピーが発生しなくなるのです。

ムーブの利用価値がわかってきたでしょうか。

上記の例で、右辺値参照を強制させたい場合、
すなわち(A)の関数を消去する場合は、以下のようになります。

std::vector<int> append(std::vector<int>&& vec){           // ...(B)
  vec.push_back(10);
  vec.push_back(20);
  vec.push_back(30);
  return std::move(vec);
}

int main(){
  std::vector<int> tmp = std::vector<int>{1, 2, 3};
  std::vector<int> x = append(std::move(tmp));             // ...(1)

  for(int elem: x){
    std::cout << elem << ", ";
  }
  std::cout << std::endl;
}

(1)は、append(tmp)とはできません。右辺値参照の関数(B)しか定義されていないので、
左辺値(保存された値)であるtmpはそのまま引数として渡せないからですね。

そこで、ムーブを使って、
tmpを右辺値(保存されない値)に、いわば「キャスト」して、
引数として渡すわけです。

(1)の行以降は、tmpは使用できません。(動作未定義です。)
tmpは右辺値(保存されない値)になってしまったから(ムーブしたから)ですね。

もしプログラム上でtmpを別で再度使用する機会があるようでしたら、
std::copy()でディープコピーし、別の変数に代入しておくなどするとよいでしょう。

まとめ

  • 右辺値とは
    • 保存されない値。メモリ上どこにも保持されない値。
    • 左辺値はその逆。保存された値。メモリ上に保持されている値。
  • 右辺値参照(&&)とは
     - 右辺値を、右辺値として受け取れる型。
     - リソース的に無駄なコピーを防ぐことができる。
  • ムーブ(std::move())とは
    • 変数へ値を代入したのち、代入した左辺値を右辺値に変更することができる。
    • ムーブされた変数は、以降は使用できない。

最後に

この記事が、誰かの役に立つことを願います。
C++の勉強、頑張ってください。

また、皆様、誤りのご指摘ありがとうございます。私も勉強になります。
誤りや改善点等がありましたら、ぜひコメント等でお教えください。随時修正いたします。

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?