M.Hiroi's Home Page

Linux Programming

お気楽C++プログラミング超入門

[ PrevPage | C++ | NextPage ]

右辺値参照 (rvalue reference)

今回は最近の規格 (C++11) で導入された「右辺値参照 (rvalue reference)」という機能について説明します。

●左辺値と右辺値

C/C++の場合、左辺値 (lvalue) はもともと「代入式の左側に置けるもの」という意味で使われていて、特定のメモリ領域を指し示す式が左辺値になります。たとえば、変数、ポインタの間接参照、配列の添字演算子などが左辺値になります。C++であれば参照や参照を返す関数も左辺値になります。

右辺値 (rvalue) は代入式の右側に置かれる値のことで、左辺値を含めすべての式が右辺値になります。式は評価されて値となりますが、このとき値を保持するための一時的なデータが生成されます。たとえば int x = 1 + 2; であれば、右辺式 1 + 2 の計算結果 3 をどこかのメモリ領域 [*1] に保持します。それから、その値を左辺値 (変数 x) が示すメモリ領域に代入します。これはインスタンスの場合も同じで、右辺式で生成されるインスタンスは一時的なものになります。その後、生成された一時的なデータは廃棄されます。

C++の場合、一時的なインスタンスを関数に参照渡しするとき、const を指定しないとコンパイルエラーになります。今までは、すぐに廃棄される一時的なインスタンスの内容を変更するのは意味がない、と考えられてきました。次の例を見てください。

リスト : 右辺値の参照渡し

#include <iostream>
using namespace std;

struct Foo { };

void foo(const Foo& x) { }
void bar(Foo& x) { }

int main()
{
  Foo a;
  foo(a);     // OK
  foo(Foo());
  bar(a);
  bar(Foo()); // NG
}

関数 foo() の引数は const を指定しています。当然ですが、foo(a) で変数 a の参照を受け取ることができます。foo(Foo()) の場合、コンストラクタ Foo() で一時的なインスタンスが生成され、その参照が foo() に渡されます。関数 bar() は引数に const を指定していないので、bar(Foo()) のように一時的なインスタンスを渡すとコンパイルエラーになります。

-- note --------
[*1] コンパイラの最適化によっては、メモリではなく CPU の「レジスタ (register)」に保持される場合もあります。

●右辺値参照

C++11 では、右辺値の参照を受け取るために新しい参照型が定義されました。データ型 T の右辺値参照は && を付けて T&& とします。この場合、左辺値の参照を受け取ることはできません。また、const を付けるとコンパイルエラーになります。右辺値参照を使うと、引数に渡されたインスタンスのメンバ変数を書き換えることができます。次の例を見てください。

リスト : 右辺値参照 (sample2101.cpp)

#include <iostream>
using namespace std;

struct Foo {
  int a;
};

void foo(const Foo& x) {
  cout << x.a << endl;
}

void bar(Foo& x) {
  x.a *= 10;
  cout << x.a << endl;
}

void baz(Foo&& x) {
  x.a *= 20;
  cout << x.a << endl;
}
  
int main()
{
  Foo a = {1};
  foo(a);
  foo(Foo{2});
  bar(a);
  //bar(Foo()); // NG
  //baz(a);     // NG
  baz(Foo{3});
}

関数 baz() は右辺値参照を受け取ります。これで、受け取ったインスタンスのメンバ変数 x の値を書き換えることができます。

実行結果を示します。

$ clang++ sample2101.cpp
$ ./a.out
1
2
10
60

●所有権の移動

C++では、自分で取得したメモリ領域はデストラクタで解放するのが普通です。この場合、メモリの所有権はそれを保持しているインスタンスが持っていると考えることができます。コピーコンストラクタはその中身を新しいメモリ領域にコピーしますが、コピー元のインスタンスが不要になる (廃棄される) ことがわかっているならば、中身をコピーせずにポインタ変数のつけ替えだけで済ますことができるはずです。

一時的に生成されたインスタンスはすぐに廃棄されるので、この場合に当てはまります。つまり、メモリの所有権を他のインスタンスに移すわけです。参考 URL によると、このような考え方を「所有権の移動」とか「ムーブセマンティクス (Move Semantics)」と呼びます。

たとえば、次のプログラムを見てください。

リスト : マッピング (sample2102.cpp)

#include <iostream>
#include <vector>
#include <list>
using namespace std;

template<class C, class F>
C mapcar(const C& seq, F func)
{
  C temp;
  for (const auto& x : seq) temp.push_back(func(x));
  return temp;
}

template<class T>
T square(T x) { return x * x; }

int main()
{
  vector<int> a = {1, 2, 3, 4, 5, 6, 7, 8};
  list<double> b = {1.1, 2.2, 3.3, 4.4, 5.5};
  vector<int> c = mapcar(a, square<int>);
  for (int x : c) cout << x << " ";
  cout << endl;
  list<double> d = mapcar(b, square<double>);
  for (double x : d) cout << x << " ";
  cout << endl;
}
$ clang++ sample2102.cpp
$ ./a.out
1 4 9 16 25 36 49 64
1.21 4.84 10.89 19.36 30.25

関数 mapcar はコンテナクラス seq の各要素に関数 func を適用し、その結果を新しいコンテナクラス temp に格納し、return でそれを返します。このような操作を「マッピング (写像)」といいます。

マッピングは関数型言語でよく使われる高階関数です。名前は Common Lisp の関数 mapcar から拝借しました。他の関数型言語では map と呼ばれることが多いです。auto に & を付けると、推論したデータ型の参照になります。const を付けると、const のデータ型になります。

C++11 よりも前のC++では、mapcar に大きなコンテナを渡すと、効率がとても悪くなることがあります。mapcar の返り値で変数を初期化する場合、コピーコンストラクタで temp の要素が新しいメモリ領域にコピーされます。変数に代入する場合は、代入演算子で同様な処理が行われます。temp は局所変数なので、関数の実行が終了すると temp は廃棄されます。つまり、temp で取得した大量のメモリは無駄になるわけです。もちろん、コンテナのサイズが大きければ、要素のコピーにも時間がかかるでしょう。

このような場合、temp が確保したメモリの所有権を移動すれば、mapcar を効率的に実行することが可能です。temp は関数が実行されている間だけ有効な一時的なインスタンスです。つまり、mapcar の返り値は右辺値として扱うことができるわけです。

このとき、廃棄されるインスタンスのポインタ変数を nullptr (または 0) に書き換えておかないと、デストラクタが実行されたときにメモリ領域が解放されてしまいます。移したメモリ領域は他のインスタンスで使用中なので、勝手に解放されてしまうと使っている側が困ってしまいます。つまり、右辺値のインスタンスを書き換えることができないと、所有権を移動することはできないのです。

●ムーブコンストラクタとムーブ代入演算子

所有権の移動にコピーコンストラクタや代入演算子を利用することはできません。どちらの場合も引数のデータ型に const を付けるのが普通ですが、const を外しても呼び出すことは可能です。そうすると、引数 (インスタンス) のメンバ変数を書き換えることができるので、所有権を移動することができるように思われますが、今度は普通のコピー処理や代入処理が必要になるときに困ってしまいます。

そこで、右辺値参照型を使うことにします。引数のデータ型が右辺値参照のコピーコンストラクタを「ムーブコンストラクタ」とか「移動コンストラクタ」といい、右辺値参照の代入演算子を「ムーブ代入演算子」とか「移動代入演算子」といいます。データ型を T とすると、これらのメンバ関数のプロトタイプは次のようになります。

  1. T(T&&);
  2. T& operator=(T&&);

1 がムーブコンストラクタ、2 がムーブ代入演算子になります。clang++ の標準ライブラリ (STL) は C++11 に対応しているので、mapcar のようなプログラムでも効率的に実行することができます。

簡単な例を示しましょう。次のリスを見てください。

リスト : ムーブコンストラクタとムーブ代入演算子の使用例 (sample2103.cpp)

#include <iostream>
#include <cstring>
using namespace std;

class cstr {
  char* str;
public:
  cstr() : str(0) {}
  cstr(const char* s) : str(new char [strlen(s) + 1]) {
      strcpy(str, s);
  }
  ~cstr() { delete[] str; }
  // コピーコンストラクタ
  cstr(const cstr& s) : str(new char [strlen(s.str) + 1]) {
    cout << "copy\n";
    strcpy(str, s.str);
  }
  // 代入演算子
  cstr& operator=(const cstr& s) {
    if (this != &s) {
      cout << "set\n";
      delete[] str;
      str = new char [strlen(s.str) + 1];
      strcpy(str, s.str);
    }
    return *this;
  }
  // ムーブコンストラクタ
  cstr(cstr&& s) : str(s.str) {
    cout << "move copy\n";
    s.str = nullptr;
  }
  // ムーブ代入演算子
  cstr& operator=(cstr&& s) {
    if (this != &s) {
      cout << "move set\n";
      delete[] str;
      str = s.str;
      s.str = nullptr;
    }
    return *this;
  }
  // 出力演算子
  friend ostream& operator<<(ostream& output, const cstr& s) {
    output << s.str;
    return output;
  }
};

クラス cstr はCスタイル文字列をメンバ変数 str に格納します。コピーコンストラクタはコピー元の文字列 s.str を自分のメンバ変数 str にコピーします。代入演算子は自分の文字列 str を delete[] で削除してから、再度 new でメモリ領域を取得して、そこに文字列 s.str をコピーします。

これに対し、ムーブコンストラクタは移動元のポインタ変数 s.str の値を自分自身の str にセットするだけです。そして、s.str を nullptr に書き換えます。同様に、ムーブ代入演算子も str を delete[] で解放してから、s.str の値を str にセットし、s.str を nullptr に書き換えます。

それでは実際に動かしてみましょう。次のリストを見てください。

リスト : 簡単なテスト (sample2103.cpp)

int main()
{
  cstr a("foo");
  cstr b("bar");
  {
    cstr c = a;
    a = b;
    b = c;
  }
  cout << a << endl;
  cout << b << endl;
  {
    cstr c = move(a);
    a = move(b);
    b = move(c);
  }
  cout << a << endl;
  cout << b << endl;
}
$ clang++ sample2103.cpp
$ ./a.out
copy
set
set
bar
foo
move copy
move set
move set
foo
bar

変数 a, b の値を変数 c を使って交換します。cstr c = a; の場合、変数 a の値が不要になるとは限らないので、ここではムーブコンストラクタは呼び出されず、通常のコピーコンストラクタが呼び出されます。同様に、a = b; と b = c; も通常の代入演算子が呼び出されます。変数 a と b の値を交換するだけなので、ムーブコンストラクタを呼び出したほうが効率的です。

このような場合、左辺値の変数を右辺値参照に型変換 (キャスト) します。いちいち static_cast を記述するのは面倒なので、それを行ってくれる関数 move() が用意されています。cstr c = move(a); とすると、ムーブコンストラクタが呼び出されます。a = move(b), b = move(c) とすると、ムーブ代入演算子が呼び出されます。これで変数 a, b のメンバ変数 str の値を交換することができます。また、move() を使うことで、所有権を移動していることがプログラム上でも明確になります。

●コンテナクラスとムーブ代入演算子

C++11 に対応している標準ライブラリ (STL) であれば、コンテナクラスにデータをセットするとき、それが右辺値であればムーブ代入演算子が呼び出されます。また、メンバ関数にも右辺値参照に対応しているものがあります。たとえば、vector<T> の push_back() は引数の型が const T& だけではなく、T&& でも受け付けてくれます。

簡単な例を示しましょう。次のリストを見てください。

リスト : vector のムーブ代入演算子 (sample2104.cpp)

#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

// Cスタイル文字列
class cstr {
  //
  // ・・・ 省略 ・・・
  //
};

int main()
{
  vector<cstr> a;
  a.push_back(cstr("foo"));
  a.push_back(cstr("bar"));
  a.push_back(cstr("baz"));
  a[0] = cstr("FOO");
  a[1] = cstr("BAR");
  a[2] = cstr("BAZ");
  while (!a.empty()) {
    cstr x = move(a.back());
    cout << x << endl;
    a.pop_back();
  }
}
$ clang++ sample2104.cpp
$ ./a.out
move copy
move copy
copy
move copy
copy
copy
move set
move set
move set
move copy
BAZ
move copy
BAR
move copy
FOO

push_back() で要素を末尾に挿入するとき、データが右辺値であればムーブコンストラクタ (move copy と表示) が呼び出されます。ベクタの容量を拡張するとき要素がコピーされますが、clang++ の STL ではコピーコンストラクタ (copy と表示) が呼び出されています。必要な大きさがわかっているならば、メンバ関数 reserve() であらかじめ容量を増やしておくといいでしょう。

a[0] = cstr("FOO") の右辺値は一時的なインスタンスなので、ムーブ代入演算子 (move set と表示) が呼び出されます。a.back() で末尾データを参照するとき、move() でキャストするとムーブ代入演算子が呼び出されます。このとき、ベクタ内のインスタンスはメンバ変数 str が nullptr に書き換えられます。所有権を移動したので、このデータを使用してはいけません。a.pop_back() で末尾要素が取り除かれ、ここでデストラクタが実行されます。

●Emplacement

C++11 では、インスタンスをコンテナクラスに挿入するとき、メンバ関数 emplace() や emplace_back() などを使うと、コンテナ内でインスタンスを生成することができます。この機能を Emplacement といいます。

void emplace(iterator, コンストラクタの引数, ...);
void emplace_back(コンストラクタの引数, ...);

簡単な使用例を示します。

リスト : emplace_back の使用例 (sample2105.cpp)

#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

class cstr {
  //
  // ・・・ 省略 ・・・
  //
};

int main()
{
  vector<cstr> a;
  a.reserve(4);
  a.emplace_back("foo");
  a.emplace_back("bar");
  a.emplace_back("baz");
  while (!a.empty()) {
    cstr x = move(a.back());
    cout << x << endl;
    a.pop_back();
  }
}
$ clang++ sample2105.cpp
$ ./a.out
move copy
baz
move copy
bar
move copy
foo

emplace_back を使うと一時的なインスタンスが生成されないので、ムーブコンストラクタは呼び出されません。また、一時的なインスタンスを生成すると、それを廃棄するためにデストラクタが呼び出されますが、その呼び出しもありません。効率的にインスタンスをコンテナに挿入することができます。

●参考 URL

  1. 本の虫: rvalue reference 完全解説, (江添亮さん)
  2. ゲーム開発者のための C++11 / C++14, (Ryo Suzuki さん)

初版 2015 年 10 月 10 日
改訂 2023 年 4 月 9 日

スマートポインタ

今回は「スマートポインタ (Smart Pointer)」を取り上げます。

以前のC++には、標準ライブラリの <memory> に auto_ptr<T> というスマートポインタが用意されてました。最近の規格 (C++11) では、auto_ptr<T> の使用は推奨されておらず、新しいスマートポインタ unique_ptr<T>, shared_ptr<T>, weak_ptr<T> が導入されました。今回はこの中から unique_ptr<T> の基本的な使い方について簡単に説明します。

●スマートポインタとは?

C++の場合、動的なメモリの取得と解放は new と delete で行います。ポインタをそのまま扱う場合、delete を忘れたときにメモリリークする危険性があり、メモリ管理はけっこう面倒なものになります。スマートポインタは名前が表しているように、ポインタをスマートに扱うためのクラステンプレートです。

スマートポインタはポインタによって初期化され、ポインタと同じように扱うことができます。また、スマートポインタにはデストラクタがあるので、デストラクタを実行するとき、ポインタが指し示しているメモリを解放することができます。

auto_ptr<T> はポインタ T* を保持し、それを「所有権の移動」で管理しています。コピーコンストラクタや代入演算子の処理が実行されると、そのポインタの所有権はコピー先 (または代入先) に移り、元の auto_ptr が保持しているポインタ変数は 0 に書き換えられます。

auto_ptr は C++11 よりも前の規格で作られたクラステンプレートなので、ムーブコンストラクタやムーブ代入演算子はありません。所有権を移動したことを忘れて、うっかり元の auto_ptr にアクセスするとコアダンプが発生します。次の例を見てください。

リスト : auto_ptr の使用例 (sample2106.cpp)

#include <iostream>
#include <memory>
using namespace std;

class Foo {
  int x;
public:
  Foo(int n) : x(n) { cout << "constructor\n"; }
  ~Foo() { cout << "destructor\n"; }
  int get() const { return x; }
};

int main()
{
  auto_ptr<Foo> a(new Foo(1));
  cout << a->get() << endl;
  auto_ptr<Foo> b = a;  // 所有権の移動
  cout << b->get() << endl;
  // cout << a->get() << endl; コアダンプ
  auto_ptr<Foo> c;
  c = b;                // 所有権の移動
  cout << c->get() << endl;
  // cout << b->get() << endl; コアダンプ
}
$ clang++ sample2106.cpp
・・・ warning (省略) ・・・

$ ./a.out
constructor
1
1
1
destructor

コンパイルするとワーニングが表示されますが、プログラムは実行することができます。auto_ptr のコンストラクタにはポインタを渡します。これで auto_ptr はそのポインタが示すメモリ領域を保持し、自分に所有権があればデストラクタでメモリ領域を解放します。

変数 a を変数 b の初期値として指定すると、コピーコンストラクタが呼び出されますが、このとき auto_ptr はメモリの所有権を移動します。a->get() を実行するとコアダンプが発生します。同様に、c = b は代入演算子の処理が呼び出され、ここでも auto_ptr の所有権は移動します。このあと、b->get() を実行してもコアダンプが発生します。main() が終了すると、デストラクタが実行されます。

このように、auto_ptr はうっかり所有権を移動すると、そのあとでコアダンプを発生させる危険性があります。また、コピー元や代入元のインスタンスを破壊的に修正するため、auto_ptr を標準的なコンテナクラスに格納することは推奨されていません。

●unique_ptr の基本的な使い方

C++11 から導入された unique_ptr は、ただ一つの unique_ptr だけがメモリの所有権を所持しているスマートポインタです。具体的には、auto_ptr と同様に所有権の移動で所持したポインタを管理します。auto_ptr と違って、unique_ptr を他の unique_ptr にコピーしたり代入することはできません。そのかわり、ムーブコンストラクタとムーブ代入演算子が用意されているので、move() を使って明示的に所有権を移動することができます。

簡単な使用例を示しましょう。

リスト : unique_ptr の使用例 (sample2107.cpp)

#include <iostream>
#include <memory>
using namespace std;

class Foo {
  int x;
public:
  Foo(int n) : x(n) { cout << "constructor\n"; }
  ~Foo() { cout << "destructor\n"; }
  int get() const { return x; }
};

int main()
{
  unique_ptr<Foo> a(new Foo(1));
  cout << a->get() << endl;
  // unique_ptr<Foo> b = a; コンパイルエラー
  unique_ptr<Foo> b = move(a);  // 明示的に所有権を移動
  cout << b->get() << endl;
  unique_ptr<Foo> c;
  // c = b;  コンパイルエラー
  c = move(b);  // 明示的に所有権を移動
  cout << c->get() << endl;  
}
$ clang++ sample2107.cpp
$ ./a.out
constructor
1
1
1
destructor

unique_ptr を使うときはヘッダファイル memory をインクルードしてください。所有権は move() で移動することができます。コピーコンストラクタや代入演算子を呼び出す処理はコンパイルエラーになります。

●配列を所持する方法

auto_ptr は配列を所持することはできませんが、unique_ptr ならば可能です。次のように、テンプレート仮引数に T [] を指定してください。

unique_ptr<T []> a(new T [size]);

あとはコンストラクタの引数に、動的に生成した配列を渡すだけです。この場合、添字演算子 [] を使って配列の要素にアクセスすることができます。簡単な例を示しましょう。

リスト : 配列を所持する (sample2108.cpp)

#include <iostream>
#include <memory>
using namespace std;

int main()
{
  unique_ptr<int []> a(new int [10]);
  for (int i = 0; i < 10; i++) a[i] = i;
  for (int i = 0; i < 10; i++) cout << a[i] << endl;
}
$ clang++ sample2108.cpp
$ ./a.out
0
1
2
3
4
5
6
7
8
9

もちろん、インスタンスを格納する配列も unique_ptr に格納することができます。次の例を見てください。

リスト : 配列を所持する (sample2109.cpp)

#include <iostream>
#include <memory>
using namespace std;

class Foo {
  int x;
public:
  Foo() : x(0) { cout << "constructor\n"; }
  Foo(int n) : x(n) { cout << "constructor " << n << endl; }
  ~Foo() { cout << "destructor " << x << endl; }
  int get() const { return x; }
  void put(int n) { x = n; }
};

int main()
{
  unique_ptr<Foo []> a(new Foo [8]);
  for (int i = 0; i < 8; i++) a[i].put(i);
  for (int i = 0; i < 8; i++) cout << a[i].get() << " ";
  cout << endl;
}
$ clang++ sample2109.cpp
$ ./a.out
constructor
constructor
constructor
constructor
constructor
constructor
constructor
constructor
0 1 2 3 4 5 6 7
destructor 7
destructor 6
destructor 5
destructor 4
destructor 3
destructor 2
destructor 1
destructor 0

このように、unique_ptr が廃棄されたとき、各々のインスタンスのデストラクタが実行されます。

●コンテナクラスに unique_ptr を格納する

unique_ptr は auto_ptr と違ってコンテナクラスに格納することができます。たとえば、vector に格納する場合は次のようになります。

リスト : vector に unique_ptr を格納する (sample2110.cpp)

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class Foo {
  //
  // ・・・ 省略 ・・・
  //
};

int main()
{
  vector<unique_ptr<Foo>> a;
  a.emplace_back(new Foo(1));
  a.emplace_back(new Foo(2));
  a.emplace_back(new Foo(3));
  cout << a[0]->get() << endl;
  cout << a[1]->get() << endl;
  cout << a[2]->get() << endl;
  a.pop_back();
  a.pop_back();
  a.pop_back();
}
$ clang++ sample2110.cpp
$ ./a.out
constructor 1
constructor 2
constructor 3
1
2
3
destructor 3
destructor 2
destructor 1

Foo のインスタンスを保持する unique_ptr を vector に追加する場合、push_back よりも emplace_back を使ったほうが簡単です。pop_back で vector から unique_ptr を取り出すと、unique_ptr が廃棄されて、所持しているインスタンスのデストラクタが実行されます。

●その他のメンバ関数

今まで所持しているメモリの所有権を放棄して、他のメモリの所有権を持ちたい場合はメンバ関数 reset を使います。reset は所持しているメモリを解放したあと、引数に渡されたメモリの所有権を所持します。引数なして reset を呼び出す、または引数に nullptr を渡すと、所持しているメモリを解放することができます。

なお、所有権の放棄はメンバ関数 release で行うことができます。release は所持していたポインタを返します。ポインタが指し示すメモリ領域は解放しないので注意してください。

unique_ptr が所持しているポインタはメンバ関数 get で求めることができます。get を呼び出したあとでも、unique_ptr は所有権を放棄しません。それから、unique_ptr には operator bool が多重定義されていて、条件式の中で unique_ptr を判定すると、所有権を持っていれば true を、そうでなければ false を返します。

簡単な使用例を示します。

リスト : メンバ関数の使用例 (sample2111.cpp)

#include <iostream>
#include <memory>
using namespace std;

class Foo {
  //
  // ・・・ 省略 ・・・
  //
};

int main()
{
  unique_ptr<Foo> a(new Foo(1));
  cout << a->get() << endl;
  cout << a.get() << endl;
  a.reset(new Foo(2));
  cout << a->get() << endl;
  cout << a.get() << endl;
  if (a)
    cout << "true\n";
  else
    cout << "false\n";
  Foo* b = a.release();
  if (a)
    cout << "true\n";
  else
    cout << "false\n";
  delete b;
}
$ clang++ sample2111.cpp
$ ./a.out
constructor 1
1
0x5597cf2aeeb0
constructor 2
destructor 1
2
0x5597cf2af2e0
true
false
destructor 2

reset を実行すると、所持していた Foo のインスタンスがデストラクタで解放されます。変数 a が格納しているポインタの値も変わっています。if の条件式で変数 a を判定すると true になります。次に、release で所有権を放棄します。返り値は変数 b にセットします。if の条件式で変数 a を判定すると false になります。今まで所持していた Foo のインスタンスは解放していないので、delete b で解放します。


初版 2015 年 10 月 10 日
改訂 2023 年 4 月 9 日

Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | C++ | NextPage ]