読者です 読者をやめる 読者になる 読者になる

スマートポインタの使い方 その1:unique_ptr

C++ Tips

スマートポインタって何?

C++において、operator newでメモリ領域(ヒープ領域)を動的に確保した場合、その領域はoperator deleteでプログラマが責任を持って解放してやらなければならない。しかし、deleteを書き忘れたり、例外が発生したときの処理を怠った場合など、それが正しく行われないことはよくある。正しく解放されなかった領域はOSやプロセスが使用可能なメモリ領域を「不正占拠」し、それが積もり積もると、OSやプロセスが停止する場合がある。

#include <memory>
class Mess {};
void f(Point p1, Point p2)
{
  Rectangle* r(new Rectangle(p1, p2));
  r->rotate(45);  // 矩形を45度回転
  // ...
  if (in_a_mess) throw Mess(); // 例外を投げてみる

  // ねぇ、例外が投げられたときの後始末は?
  delete r;
}

そこで、プログラマの負担を軽減すべく、そのメモリ領域の「寿命」の管理を自動的に行う「スマートポインタ」がC++の拡張ライブラリであるBoostに実装された。その後、C++の新規格であるC++0x(C++11)でBoostの実装をベースにしたスマートポインタがC++標準ライブラリに取り込まれた。*1
スマートポインタとは文字通り"smart"(気の利いた)なポインタのことで、スマートポインタが管理しているメモリ領域が不要になったら自動的に解放してくれる。*2メモリ領域を解放するタイミングの違いによりいくつかのタイプがある。

  • std::unique_ptr (boost::scoped_ptr) : 変数の寿命が尽きるとそのメモリ領域を開放する。*3
  • std::shared_ptr (boost::shared_ptr) : 参照カウントを持ち、参照カウントがゼロになるとそのメモリ領域を開放する。
  • boost::intrusive_ptr : プログラマが参照カウントを管理する。
  • boost::scoped_array : boost::scoped_ptrの配列版
  • boost::shared_array : boost::shared_ptrの配列版

もちろん、振る舞いはあくまでも「ポインタ」なので、見た目は通常のポインタ関連のコードと何ら変わりが無い。

#include <memory>
class Mess {};
void f(Point p1, Point p2)
{
  std::unique_ptr<Rectangle> r(new Rectangle(p1, p2)); // unique_ptrはブロックの外に達するとメモリ領域を自動開放する
  r->rotate(45);  // 矩形を45度回転
  
  // ...
  if (in_a_mess) throw Mess(); // 例外を投げてみる
  // 途中で例外が投げられてもブロックの外に出たら自動的にメモリ領域の開放を行う
  // operator deleteは不要。スマートポインタが解放する。
}

std::unique_ptrを使ってみる

std::unique_ptrはブロックの外に抜けるなど変数の寿命が尽きたら、管理しているメモリ領域を自動的に開放するスマートポインタ。その名の通り"unique"(唯一)のメモリ領域しか管理しないので、複数のunique_ptrのインスタンスが同一のメモリ領域を管理することはあり得ない。元々、C++の標準ライブラリにはスマートポインタとしてauto_ptrがあったが、仕様に少々問題があった為にC++0xからは使用が非推奨となった。その替わりとしてunique_ptrが導入された。
使い方はコンストラクタの引数にoperator newで生成したポインタを渡すだけ。*4以後、そのメモリ領域はunique_ptrの管理下に置かれる。引数を与えなかった場合は、unique_ptrが管理しているメモリ領域は「無し」とみなす。使用するシチュエーションとして考えられるのは、pimplイディオムの実装部分や、Abstract Factoryパターンが生成したオブジェクトを自動的に解放したい場合など。

#include <iostream>
#include <memory>
class TestObject
{
public:
    TestObject()
    {
        std::cout << "TestObject::Constructor" << std::endl;
    }
    ~TestObject()
    {
        std::cout << "TestObject::Destructor" << std::endl;
    }
    void message()
    {
        std::cout << "TestObject::message" << std::endl;
    }
};

int main()
{
    std::unique_ptr<TestObject> pObject(new TestObject()); // newしたオブジェクトをコンストラクタに渡す
    pObject->message(); // メンバ関数の呼び出し
    return 0;
}

メンバへのアクセスはポインタと同様にアロー演算子(operator->)で行う。インスタンスへのアクセスはポインタ演算子(operator*)。当然、deleteも必要ない。ブロックの外に出ると変数の寿命が尽きるのでunique_ptrはデストラクタで自身が管理しているメモリ領域を開放する。ただし、operator=で代入することも出来ない。

void f(Point p1, Point p2)
{
  std::unique_ptr<Rectangle> rb = new Rectangle(p1, p2); // 代入は駄目。初期化は必ずコピーコンストラクタで。
}

配列として連続した複数個のメモリ領域が欲しい場合は、[]をつけて宣言する。

#include <iostream>
#include <memory>
int main()
{
  std::unique_ptr<int[]> a(new int[10]);

  for (int i = 0; i < 10; ++i) {
	a[i] = i;
  }

  for (int i = 0; i < 10; ++i) {
	std::cout << a[i] << std::endl;
  }
  return 0;
}

生ポインタの取得

unique_ptrはあくまでもポインタをラップするユーティリティクラスである。コードの見た目をポインタのように見せているだけであり、型そのものが違う。Windows APIのような標準C++ではないライブラリやAPIを呼び出す場合、unique_ptrが管理しているメモリ領域の生ポインタが必要になる場合がある。unique_ptrが管理しているメモリ領域の生ポインタを取得するにはgetメソッドを使用する。注意しなければならないのはgetメソッドはメンバ演算子(operator.)で呼び出す。

// 地図の名前を取得するAPI
int get_map_name(int MapID, char* buffer);
void process_map_name(int MapID)
{
  int length = get_map_name(MapID, NULL);  // 必要な領域サイズを取得
  std::unique_ptr<char[]> buffer(new char[length]);  // 領域を確保

  get_map_name(MapID, buffer); // エラー!!!
  get_map_name(MapID, buffer.get()); // getメソッドで生ポインタを取得する
}

取得した生ポインタはあくまでもunique_ptrの管理下にあるので、自分でdeleteしてはならない。unique_ptrがメモリ領域を管理しているか否かは、getメソッドで生ポインタを取得するか、あるいは、operator boolでチェックする。

void f() 
{
  std::unique_ptr<TestObject> obj;  // どこも管理していないunique_ptr
  if (!obj) {
    std::cout << "ptr is not enabled" << std::endl;
  }
}

unique_ptrの再初期化

Abstract Factoryパターンのようにサブクラスで振る舞いが変化するオブジェクトを動的に生成する場合など、文脈によってunique_ptrを再初期化する必要が出てくる。unique_ptrはoperator=で代入(上書き)出来ないが、resetメソッドで別のポインタで再初期化できる。このとき、unique_ptrは自身が管理しているメモリ領域を解放するので、プログラマがdeleteでメモリ領域を明示的に開放する必要は無い。

#include <iostream>
#include <memory>

class TestObject
{
public:
  TestObject(int ID) : m_ID(ID)
  {
    std::cout << "TestObject::Constructor" << "ID = " << m_ID << std::endl;
  }
  ~TestObject()
  {
    std::cout << "TestObject::Destructor" << "ID = " << m_ID << std::endl;
  }
  void message()
  {
    std::cout << "TestObject::message" << std::endl;
  }
  int m_ID;
};

int main()
{
  std::unique_ptr<TestObject> pObject(new TestObject(100)); 
  pObject->message(); // メンバ関数の呼び出し

  // 再初期化
  pObject.reset(new TestObject(200));
  return 0;
}

上記コードの実行結果

TestObject::ConstructorID = 100
TestObject::message
TestObject::ConstructorID = 200
TestObject::DestructorID = 100
TestObject::DestructorID = 200

resetの引数にNULLポインタを渡せば、明示的にunique_ptrが管理しているメモリ領域を解放することが出来る。メモリ領域を開放せずに、ただ単にunique_ptrとメモリ領域の関係を切り離す場合はreleaseメソッドを使用する。戻り値としてメモリ領域の生ポインタが返るので、その後は通常のポインタとして扱える。当然、解放が必要であれば明示的に解放しなければならない。

void f()
{
  std::unique_ptr<TestObject> pObject(new TestObject()); 
  TestObject* pp = pObject.release(); // unique_ptrとメモリ領域の関係を切り離す

  delete pp; // 明示的に解放しないとリソースリークする
}

unique_ptrと代入演算子(operator=)

C++03まで標準であったauto_ptrが「問題」とされたのは、auto_ptr同士をoperator=で代入するとメモリ領域の所有者が=の右から左へ移り、代入元のauto_ptrの管理領域はNULLとなる為である。

#include <iostream>
#include <memory>
int main()
{
  std::auto_ptr<int> a(new int(100));
  std::auto_ptr<int> b(new int(200));

  // 単なるポインタ同士の代入のつもりだが、実はポインタの所有権が移動する
  b = a;
  
  // aはどこも参照していない抜け殻なので、Access Violationが発生
  std::cout << *a << std::endl;

  return 0;
}

一見問題なさそうなコードだが、単純な代入文であるにもかかわらず副作用が発生するのはよろしくないので、unique_ptrはoperator=を隠蔽して明示的な代入を出来なくした。

#include <iostream>
#include <memory>
int main()
{
  std::unique_ptr<int> a(new int(100));
  std::unique_ptr<int> b;

  b = a; // operator=は隠蔽されているのでコンパイルエラーが発生する。
  std::cout << *a << std::endl;

  return 0;
}

unique_ptrをメンバに持つクラスにoperator=を定義する場合など、文脈によってはunique_ptr同士で「値」をやりとりしなければならない場合がある。unique_ptrは単一の領域しか管理しないので、単一性を担保するための戦略として「中身」のやりとり以外に、管理しているメモリ領域のポインタ値の交換、もしくは譲渡が考えられる。

#include <iostream>
#include <memory>
int main()
{
  std::unique_ptr<int> a(new int(100));
  std::unique_ptr<int> b(new int(200));

  std::cout << "a = " << *a << std::endl;
  std::cout << "b = " << *b << std::endl;

  *a = *b;  // unique_ptrの管理下にある領域の「中身」をやりとりする

  std::cout << "a = " << *a << std::endl;
  std::cout << "b = " << *b << std::endl;

  return 0;
}

unique_ptrが管理している領域のポインタ値を「交換」する場合はswapメソッドを使う。

#include <iostream>
#include <memory>
int main()
{
  std::unique_ptr<int> a(new int(100));
  std::unique_ptr<int> b(new int(200));

  std::cout << "a = " << *a << std::endl;
  std::cout << "b = " << *b << std::endl;
  a.swap(b);  // unique_ptrが管理している領域を交換
  std::cout << "a = " << *a << std::endl;
  std::cout << "b = " << *b << std::endl;

  return 0;
}

ポインタを「譲渡」する場合は、C++0xで導入されたmove関数を使用する。

#include <iostream>
#include <memory>
int main()
{
  std::unique_ptr<int> a(new int(100));
  std::unique_ptr<int> b;

  if (a) std::cout << "a = " << *a << std::endl;
  if (b) std::cout << "b = " << *b << std::endl;
  // std::auto_ptrだと、b = a;に相当
  b = std::move(a); 	// unique_ptrが管理しているポインタを譲渡
  if (a) std::cout << "a = " << *a << std::endl;
  if (b) std::cout << "b = " << *b << std::endl;

  return 0;
}

move関数はオブジェクトが保持しているリソースを戻り値に譲渡*5させるヘルパ関数で、結果、譲渡元には何も残らない。C++0xで導入された「ムーブセマンティクス」と「rvalue参照」を用いて実装されている。

*1:よって、名前空間はstd(std::tr1)である。

*2:厳密にはヒープ領域だけでなく、ハンドルといったメモリとは少々違う意味合いのリソースも管理できるが、これは別項で。

*3:std::unique_ptrとboost::scoped_ptrはコンストラクタ等の実装の違いはあるけど、ほぼ同一と言っていいと思う。

*4:他に解放処理を決定するdeleterがあるが、滅多に使わないので省略。

*5:厳密には、lvalueをrvalueに変換