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

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

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++11より前の「古い」C++はC由来であるためにメモリなどのリソースの管理はプログラマに委ねられていた。例えば、Cで可変長配列を実装するとなると本当に面倒。
そこで、C++11でプログラマの負担を軽減すべく、メモリ領域の「寿命」の管理を自動的に行う「スマートポインタ」が実装された。
スマートポインタとは文字通り"smart"(気の利いた)なポインタのことで、スマートポインタが管理しているメモリ領域が不要になったら自動的に解放してくれる。*1メモリ領域を解放するタイミングの違いにより2種類のタイプがある。

  • std::unique_ptr : 変数の寿命が尽きるとそのメモリ領域を自動的に開放する。
  • std::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++11からは使用が非推奨となり、C++17で削除された。その替わりとしてunique_ptrが導入された。
使い方はstd:make_unique関数を使ってオブジェクトを生成するだけ。*2以後、そのオブジェクトが占有するメモリ領域は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{std::make_unique<TestObject>()} // オブジェクトをコンストラクタに渡す
    pObject->message(); // メンバ関数の呼び出し
    return 0;
}

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

void f(Point p1, Point p2)
{
  std::unique_ptr<Rectangle> rb{std::make_unique<>(p1, p2)}; // 初期化はmake_uniqueで
}

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

#include <iostream>
#include <memory>
int main() {
  constexpr int kArraySize{10};
  std::unique_ptr<int[]> a{std::make_unique<int[]>(kArraySize)};  // 10要素分生成

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

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

生ポインタの取得

unique_ptrはあくまでもポインタをラップするユーティリティクラス。コードの見た目をポインタのように見せているだけであり、型そのものが違う。Windows APIUnix/Linuxシステムコールのような標準C++ではないAPIやCのライブラリを呼び出す場合、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{std::make_unique<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の引数にnullptrを渡せば、明示的にunique_ptrが管理しているメモリ領域を解放することが出来る。メモリ領域を開放せずに、ただ単にunique_ptrとメモリ領域の関係を切り離す場合はreleaseメソッドを使用する。戻り値としてメモリ領域の生ポインタが返るので、その後は通常のポインタとして扱える。当然、解放が必要であれば明示的に解放しなければならない。

void f()
{
  std::unique_ptr<TestObject> pObject{std::make_unique<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{std::make_unique<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{std::make_unique<int>(100)};
  std::unique_ptr<int> b{std::make_unique<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{std::make_unique<int>(100)};
  std::unique_ptr<int> b{std::make_unique<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++11で導入されたmove関数を使用する。

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

  if (a) std::cout << "a = " << *a << std::endl;
  if (b) std::cout << "b = " << *b << std::endl;
  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関数はオブジェクトが保持しているリソースを戻り値に譲渡*3させるヘルパ関数で、結果、譲渡元には何も残らない。C++11で導入された「ムーブセマンティクス」と「rvalue参照」を用いて実装されている。

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

*2:newでオブジェクトを生成しても良いのだけど静的解析で指摘される場合がある。

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