Cpp 右辺値参照

提供:yonewiki

C++に戻る


本来の表記は「C++(Cpp) 右辺値参照」です。この記事に付けられた題名はテンプレート:記事名の制約から不正確なものとなっています。

※このページではC++にのみ存在する機能として、記事タイトルがC++ 右辺値参照になっています。

右辺値参照

Cpp 参照の特殊なパターンとして右辺値参照があります。あまり知られていないという点で特殊と記述しましたが、知っているひとからすれば、ごく自然なC++の言語仕様の一部に過ぎないです。


右辺値というとa = b;のように書くとbが方程式の右辺値です。このようなものの参照?って言われてもピンとこないかもしれませんが、よく右辺値にくるものの参照という意味で、オブジェクトをもたない値の参照というような技術になります。オブジェクトをもたない値というのは具体的には、以下のようなものの右辺にくるものです。

int a = 256;
int a = function();

通常はメモリのどこかにおかれるものですが、意識したことの無いようなものです。


こういうものを参照するための型として変数の型名の後ろに&&記号をつけて宣言する型を右辺値参照と呼びます。具体的には以下のように使うことができます。

int&& rrefnSize = 256;
printf("rrfnSize Addres->%x, rrefnSize Value->%d\n",&rrefnSize,rrefnsize);

出力結果

rrfnSize Addres->46f85c, rrefnSize Value->256

だから、何?普通のint型と何も変わらないような感じですよね。でもこの参照は右辺値を参照するためのものなので、通常の変数を渡すとコンパイルエラーになります。これははっきりと通常ポインタ変数で扱わないものを型として区別できるとことが、この右辺値参照の活用の道となっています。右辺値というは通常は変数にはとっておかなくていい、一時的な値です。その通常はとっておかなくていいような値を右辺値参照によって取り出して引数として受け取り、それを活用しようとする技術です。

int nSize;
int&& rrefnSize = nSize; //これは駄目。

このようにすると以下のようなコンパイルエラーとなります。

error C2440: '初期化中' : 'int' から 'int &&' に変換できません。

VC++なら記述した段階で、不正な代入としてチェックされ、nSizeの文字の下に赤い波線が付きます。


以下は キャストによる明示的な右辺値参照代入の指定です。

int nSize;
int&& rrefnSize = static_cast<int&&>(nSize); //これはOK。

このようにint&&型に キャストすれば大丈夫です。これはどういう事かというと、キャストがされると結局は参照が出来るようになるというだけなので、通常の参照と変わらないです。ただ型が右辺値参照で扱われる参照になるので、このあと再度、右辺値を代入することはできて、nSizeの中身が変更されます。ただし、右辺値で無い値を入れることは出来ないので、再度、キャスト付きでしか通常の参照のやり直しをすることはできません。このようなキャストは以下のようなstd::move()というC++標準関数を指定することでも同様の動作を実現できます。

int nSize;
int&& rrefnSize = std::move(nSize); //キャストと同じで、OK。

std::moveは右辺値でない変数も右辺値としてキャストする関数です。型名を書かなくても良いので、記述しやすいというのがこの関数の利点です。右辺値参照型というのは、右辺値のような一時的にメモリに配置されるものの、それ自体は消えてしまって、次に使うことはないような値ですので、このような代入をしたということは、もうその変数での利用はしないということを宣言したようなものですから、元の値は残さなくていいという意味でmoveという名前が付いています。実際にはmove関数で代入した後も元の変数での操作が出来るので、早いところ手動で消滅させたいものですが、動的に生成しなかった変数はプログラム内で宣言した領域内の終了まで残っているので、右辺値とは再利用できないものを扱うものなので再利用しないようにした方がよいでしょう。動的に生成したものを右辺値参照でキャストした場合も元のポインタ変数にヌルを設定するようにします。場合によっては、右辺値参照によって取得した変数を利用したクラスが消滅する際に、元の値も同時に消滅して、次に元の値を保持していたクラスが消滅する際に動作するデストラクタが、元の値のポインタの実体を消滅させる処理をするときに、すでに消滅済で、エラーになる場合があります。元の値のポインタ変数を消滅させる前に右辺値参照として活用するための手順を踏まないとキャストした意味がなくなってしまいますので手順が大事になります。ここでは、右辺値にキャストした値は右辺値として振る舞うために使われるべきなのだということを理解してもらえれば良いと思います。こうした右辺値参照へのキャストの活用方法についてはもう少しあとで触れます。


以下はキャストを必要としない右辺値参照のパターンです。

class CTest
{
public:
  CTest(void){};
  ~CTest(void){};
  int getIntegerNum(void){return 1;};
  static int getIntegerNumStatic(void){return 1;};
};
int Function(){
  return 1;
}
int _tmain(int argc, _TCHAR* argv[])
{
  CTest* pCTestInstance = new CTest;
  int&& rrefnFReturn = Function();//OK。 関数の右辺値参照代入
  printf("rrefnFReturn        Addres->%x, rrefnFReturn        Value->%d\n",&rrefnFReturn,rrefnFReturn);

  int&& rrefnFReturn2 = Function();//2回目の関数の右辺値参照代入
  printf("rrefnFReturn2       Addres->%x, rrefnFReturn2       Value->%d\n",&rrefnFReturn2,rrefnFReturn2);

  int&& rrefnCReturn = pCTestInstance->getIntegerNum();//OK。クラスメンバ関数の右辺値参照代入
  printf("rrefnCReturn        Addres->%x, rrefnCReturn        Value->%d\n",&rrefnCReturn,rrefnCReturn);

  int&& rrefnCReturn2 = pCTestInstance->getIntegerNum();//OK。関数の右辺値参照代入
  printf("rrefnCReturn2       Addres->%x, rrefnCReturn2       Value->%d\n",&rrefnCReturn2,rrefnCReturn2);

  int&& rrefnStaticCReturn = CTest::getIntegerNumStatic();//クラス関数(static)の右辺値参照代入
  printf("rrefnStaticCReturn  Addres->%x, rrefnStaticCReturn  Value->%d\n",&rrefnStaticCReturn,rrefnStaticCReturn);

  int&& rrefnStaticCReturn2 = CTest::getIntegerNumStatic();//クラス関数(static)の右辺値参照代入
  printf("rrefnStaticCReturn2 Addres->%x, rrefnStaticCReturn2 Value->%d\n",&rrefnStaticCReturn2,rrefnStaticCReturn2);
  return 0;
}

出力結果

rrefnFReturn        Addres->32f898, rrefnFReturn        Value->1
rrefnFReturn2       Addres->32f880, rrefnFReturn2       Value->1
rrefnCReturn        Addres->32f868, rrefnCReturn        Value->1
rrefnCReturn2       Addres->32f850, rrefnCReturn2       Value->1
rrefnStaticCReturn  Addres->32f838, rrefnStaticCReturn  Value->1
rrefnStaticCReturn2 Addres->32f820, rrefnStaticCReturn2 Value->1

上記のように関数の返り値も右辺値として扱われますので、返り値の型が一致すれば右辺値参照型への代入ができます。構造体やクラスのメンバ関数に対してもキャストすることなく右辺値として参照を受け取ることができます。関数は呼び出される都度、新しいアドレスに処理結果が返って来ています。

このような右辺値戻り値を引数にとるような関数で、ポインタ変数のアドレスだけを移動させることによる、付け替え関数を作ることができます。言葉での説明ではイメージできないと思うので、実際にやってみた方がいいですね。

Mainプログラム

#include "stdafx.h"
#include "RightRefSample.h"

int _tmain(int argc, _TCHAR* argv[])
{
  printf("★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★\n");
  printf("★★開始\n");
  printf("\n");
  printf("★オリジナルのクラス生成 Inst001\n");
  CRightRefSample* crightrefsampleInst001 = new CRightRefSample("001");

  printf("\n");
  printf("★2個目のクラス生成 Inst001を初期値でコピーしたクラスInst002\n");
  CRightRefSample* crightrefsampleInst002 = new CRightRefSample(*crightrefsampleInst001);

  printf("\n");
  printf("★3個目のクラス生成Inst003\n");
  CRightRefSample* crightrefsampleInst003 = new CRightRefSample("003");
  
  printf("\n");
  printf("★4個目のクラス生成Inst004\n");
  CRightRefSample* crightrefsampleInst004 = new CRightRefSample("004");

  printf("★状態表示\n");
  printf("★m_pStrChar:001->%06x 002->%06x 003->%06x 004->%06x\n",
    crightrefsampleInst001->getStrValueAddress(),
    crightrefsampleInst002->getStrValueAddress(),
    crightrefsampleInst003->getStrValueAddress(),
    crightrefsampleInst004->getStrValueAddress());
  printf("★m_pStrChar:001->%6s 002->%6s 003->%6s 004->%6s\n",
    crightrefsampleInst001->getStrValueAddress(),
    crightrefsampleInst002->getStrValueAddress(),
    crightrefsampleInst003->getStrValueAddress(),
    crightrefsampleInst004->getStrValueAddress());


  printf("\n");
  printf("★3個目に生成したInst003へInst001をコピー\n");
  *crightrefsampleInst003 = *crightrefsampleInst001;

  printf("★状態表示\n");
  printf("★m_pStrChar:001->%06x 002->%06x 003->%06x 004->%06x\n",
    crightrefsampleInst001->getStrValueAddress(),
    crightrefsampleInst002->getStrValueAddress(),
    crightrefsampleInst003->getStrValueAddress(),
    crightrefsampleInst004->getStrValueAddress());
  printf("★m_pStrChar:001->%6s 002->%6s 003->%6s 004->%6s\n",
    crightrefsampleInst001->getStrValueAddress(),
    crightrefsampleInst002->getStrValueAddress(),
    crightrefsampleInst003->getStrValueAddress(),
    crightrefsampleInst004->getStrValueAddress());


  printf("\n");
  printf("★Inst001とInst003を交換\n");
  CRightRefSample::swap(*crightrefsampleInst001,*crightrefsampleInst003);
  printf("★状態表示\n");
  printf("★m_pStrChar:001->%06x 002->%06x 003->%06x 004->%06x\n",
    crightrefsampleInst001->getStrValueAddress(),
    crightrefsampleInst002->getStrValueAddress(),
    crightrefsampleInst003->getStrValueAddress(),
    crightrefsampleInst004->getStrValueAddress());
  printf("★m_pStrChar:001->%6s 002->%6s 003->%6s 004->%6s\n",
    crightrefsampleInst001->getStrValueAddress(),
    crightrefsampleInst002->getStrValueAddress(),
    crightrefsampleInst003->getStrValueAddress(),
    crightrefsampleInst004->getStrValueAddress());

  printf("\n");
  printf("★4個目に生成したInst004へInst001をムーブ\n");
  *crightrefsampleInst004 = std::move(*crightrefsampleInst001);
  printf("★状態表示\n");
  printf("★m_pStrChar:001->%06x 002->%06x 003->%06x 004->%06x\n",
    crightrefsampleInst001->getStrValueAddress(),
    crightrefsampleInst002->getStrValueAddress(),
    crightrefsampleInst003->getStrValueAddress(),
    crightrefsampleInst004->getStrValueAddress());
  printf("★m_pStrChar:001->%6s 002->%6s 003->%6s 004->%6s\n",
    crightrefsampleInst001->getStrValueAddress(),
    crightrefsampleInst002->getStrValueAddress(),
    crightrefsampleInst003->getStrValueAddress(),
    crightrefsampleInst004->getStrValueAddress());

  printf("\n");
  printf("★生成したインスタンスの全削除手動\n");
  printf("Inst001 Delete\n");
  delete crightrefsampleInst001;

  printf("Inst002 Delete\n");
  delete crightrefsampleInst002;

  printf("Inst003 Delete\n");
  delete crightrefsampleInst003;

  printf("Inst004 Delete\n");
  delete crightrefsampleInst004;
  printf("★★終了\n");
  printf("★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★\n");
  return 0;
}

RightRefSample.h

#pragma once
#include <iostream>

using namespace std;

class CRightRefSample
{
private:
  char* m_pcStrValue;
public:
  int Function(){return 1;};
  CRightRefSample(void);
  CRightRefSample(const char* pconstcDefaultValue);
  CRightRefSample(const CRightRefSample& refcrightrefsampleInst );
  CRightRefSample(CRightRefSample&& rrefcrightrefsampleInst );
  CRightRefSample& operator= (const CRightRefSample& refcrightrefsampleInst);
  CRightRefSample& operator= (CRightRefSample&& refcrightrefsampleInst);

  static void swap(CRightRefSample& refcrightrefsampleA,CRightRefSample& refcrightrefsampleB);
  char* getStrValueAddress(){return m_pcStrValue;};

  ~CRightRefSample(void);
};

RightRefSample.cpp

#include "stdafx.h"
#include "RightRefSample.h"

//コンストラクタ
CRightRefSample::CRightRefSample(void)
{
  printf("%06x: CRightRefSample (void)Constructor        :%s\n", this, __FUNCTION__);
  m_pcStrValue = new char[1024];
  strcpy_s(m_pcStrValue, 1024, "初期値");
  
}

CRightRefSample::CRightRefSample(const char* pconstcDefaultValue){
  printf("%06x: CRightRefSample (const char)Constructor  :%s\n", this, __FUNCTION__);
  
  m_pcStrValue = new char[strlen(pconstcDefaultValue) + 1];
  strcpy_s(m_pcStrValue, strlen(pconstcDefaultValue) + 1, pconstcDefaultValue);
}

//コピーコンストラクタ
CRightRefSample::CRightRefSample(const CRightRefSample& refcrightrefsampleInst ){
  printf("%06x: CRightRefSample (Ref)CopyConstructor     :%s\n", this, __FUNCTION__);
  m_pcStrValue = new char[1024];
  strcpy_s(m_pcStrValue, 1024, refcrightrefsampleInst.m_pcStrValue);
}

//ムーブコンストラクタ
CRightRefSample::CRightRefSample(CRightRefSample&& rrefcrightrefsampleInst ){
  
  printf("%06x: CRightRefSample (RightRef)MoveConstructor:%s\n", this, __FUNCTION__);
  m_pcStrValue = rrefcrightrefsampleInst.m_pcStrValue;//新しいクラスにだけ
  rrefcrightrefsampleInst.m_pcStrValue = nullptr;
}
//=代入演算子の後ろに参照引数でコピー処理
CRightRefSample& CRightRefSample::operator= (const CRightRefSample& refcrightrefsampleInst){
  delete[] m_pcStrValue;

  printf("%06x: CRightRefSample (Ref)operator=           :%s\n", this, __FUNCTION__);
  m_pcStrValue = new char[strlen(refcrightrefsampleInst.m_pcStrValue) + 1];
  strcpy_s(m_pcStrValue,strlen(refcrightrefsampleInst.m_pcStrValue) + 1,refcrightrefsampleInst.m_pcStrValue);

  return *this;
}
//=代入演算子の後ろに右辺値参照引数でムーブ処理
CRightRefSample& CRightRefSample::operator= (CRightRefSample&& refcrightrefsampleInst){
  delete[] m_pcStrValue;

  printf("%06x: CRightRefSample (RightRef)operator=      :%s\n", this, __FUNCTION__);
  m_pcStrValue = refcrightrefsampleInst.m_pcStrValue;
  refcrightrefsampleInst.m_pcStrValue = nullptr;

  return *this;
}
//クラス変数の入れ替えムーブ版
void CRightRefSample::swap(CRightRefSample& refcrightrefsampleA,CRightRefSample& refcrightrefsampleB){
  printf("%06x: CRightRefSample (Ref, Ref)swap           :%s\n", 0, __FUNCTION__);
  CRightRefSample crightrefsampleTemp = std::move(refcrightrefsampleA);//ムーブコンストラクタ
  refcrightrefsampleA = std::move(refcrightrefsampleB);//代入演算子ムーブ処理
  refcrightrefsampleB = std::move(crightrefsampleTemp);//代入演算子ムーブ処理
}

//デストラクタ
CRightRefSample::~CRightRefSample(void)
{
  printf("%06x: CRightRefSample (void)Destructor         :addres->%06x name->%s\n", this, m_pcStrValue, __FUNCTION__);
  delete[] m_pcStrValue;
}

非常に長いサンプルになってしまいましたが、こんな感じでClassのメンバ変数を入れ替えるようなプログラム(staticで定義したメンバ関数swap 静的メンバ関数はオブジェクトからでなくても呼べる関数です。別で詳細は説明の予定。thisポインタが使えないクラスの中にあるだけの関数CRightSample::swap(…)のように呼び出すものです。)を作ってみました。算術演算子 = をオーバロード(関数として定義)していまして、(CRightRefSampleのインスタンス) = の後ろに(CRightRefSampleのインスタンス)が設定された場合の関数と(CRightRefSampleのインスタンス)が右辺値参照として設定されている場合の関数の2つをオーバロードしています。同じ代入演算子でも引数が右辺値参照のときはムーブ、引数が参照のときはコピーのように使い分けることができます。右辺値参照の変数は使わなくなる予定の変数ですので、引数で受け取った変数をコピーするのではなくムーブしてしまうようにして利用します。特にswapのような入れ替えではポインタの交換だけを行うことで入れ替えられますので、AとBの入れ替え処理では入れ替え用の一時的な変数に対してAをムーブし、ムーブしおわったAにBの値をムーブ、そして一時的に作った変数からBへムーブという具合に3回のムーブによりポインタの入れ替えができて内容を交換できます。今回はひとつのメンバ変数のみの入れ替えサンプルですが、クラスの中の要素が文字列のような配列であったり、構造体、クラスといったようなもっと複雑なデータを保持している場合にムーブすることでデータの受け渡しをすることができます。コピーを作らなくてもいいような処理にはムーブは資源の節約にもなるし、無駄な処理が無いため処理速度も改善されます。ムーブのような作業は別に右辺値参照を使うことなくできますので、必ずしも右辺値参照が必要なわけではないですが、参照を引数にした場合の処理と分けることができるのが特徴です。今回はstd::moveのような関数であえて右辺値として扱うことでムーブを実現していますが、右辺値参照を引数にしている部分に引数にスタティックなクラスメンバ関数や関数を定義してクラスのインスタンスを戻り値とするような関数を作った場合に純粋な右辺値が登場しますが、そのような場合でも同じようにムーブされて問題ない処理になるはずです。右辺値参照を引数にとるムーブコンストラクタや代入演算子のオーバロードによるムーブを活用できることを知っておくことで、よりよいクラスの使い方が可能となります。同じような関数が多くなりがちですが、このあたりはテンプレートの活用によりさらにコーディングの効率はあげられると思います。ここでは、あまりややこしくならないように最小限のクラス化と、最小限のテンプレートクラスの利用でメモを作成しています。


ムーブコンストラクタはコピーコンストラクタの引数が通常の参照であったのに対して、元の値をとっておかないような使い方として、引数を右辺値参照型にしているようなコンストラクタになっています。


このプログラムを理解するにはオブジェクト(インスタンス)、ポインタ、nullptr、文字列操作、参照、クラス(コンストラクタ デストラクタ 静的メンバ関数 演算子のオーバロード オーバロード インライン関数)を理解しておく必要があります。


サンプルプログラムの出力結果は以下のとおりです。

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
★★開始

★オリジナルのクラス生成 Inst001
338240: CRightRefSample (const char)Constructor  :CRightRefSample::CRightRefSample

★2個目のクラス生成 Inst001を初期値でコピーしたクラスInst002
3382a0: CRightRefSample (Ref)CopyConstructor     :CRightRefSample::CRightRefSample

★3個目のクラス生成Inst003
3382d0: CRightRefSample (const char)Constructor  :CRightRefSample::CRightRefSample

★4個目のクラス生成Inst004
338330: CRightRefSample (const char)Constructor  :CRightRefSample::CRightRefSample
★状態表示
★m_pStrChar:001->338270 002->33dbd0 003->338300 004->338360
★m_pStrChar:001->   001 002->   001 003->   003 004->   004

★3個目に生成したInst003へInst001をコピー
3382d0: CRightRefSample (Ref)operator=           :CRightRefSample::operator =
★状態表示
★m_pStrChar:001->338270 002->33dbd0 003->338300 004->338360
★m_pStrChar:001->   001 002->   001 003->   001 004->   004

★Inst001とInst003を交換
000000: CRightRefSample (Ref, Ref)swap           :CRightRefSample::swap
24f908: CRightRefSample (RightRef)MoveConstructor:CRightRefSample::CRightRefSample
338240: CRightRefSample (RightRef)operator=      :CRightRefSample::operator =
3382d0: CRightRefSample (RightRef)operator=      :CRightRefSample::operator =
24f908: CRightRefSample (void)Destructor         :addres->000000 name->CRightRefSample::~CRightRefSample
★状態表示
★m_pStrChar:001->338300 002->33dbd0 003->338270 004->338360
★m_pStrChar:001->   001 002->   001 003->   001 004->   004

★4個目に生成したInst004へInst001をムーブ
338330: CRightRefSample (RightRef)operator=      :CRightRefSample::operator =
★状態表示
★m_pStrChar:001->000000 002->33dbd0 003->338270 004->338300
★m_pStrChar:001->(null) 002->   001 003->   001 004->   001

★生成したインスタンスの全削除手動
Inst001 Delete
338240: CRightRefSample (void)Destructor         :addres->000000 name->CRightRefSample::~CRightRefSample
Inst002 Delete
3382a0: CRightRefSample (void)Destructor         :addres->33dbd0 name->CRightRefSample::~CRightRefSample
Inst003 Delete
3382d0: CRightRefSample (void)Destructor         :addres->338270 name->CRightRefSample::~CRightRefSample
Inst004 Delete
338330: CRightRefSample (void)Destructor         :addres->338300 name->CRightRefSample::~CRightRefSample

★★終了
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★


C++に戻る