#13 リソース管理にオブジェクトを使う

Effective C++ 3章13項の内容です.

1.リソース管理オブジェクトとは?

自分みたいに組み込み系をメインでやっている人って,C++をガンガン使うというよりはCを使っている人が多いと思います.Cだとやっぱり生のポインタに対してメモリをmallocで割り当てて,使い終わったらfreeで開放して...という使い方をすることが多い思います.ただ,このメモリ周りのデバッグってめんどくさいんですよね.簡単にわからないし.C++はこのあたりの流れを組んで,あまり生のポインタをガンガン使うことが良しとされてないような気がします.ただ,そうは言ってもやっぱり使わないわけには行かないので,この時にどうやってリソースの開放忘れを防ぐか?という観点で考えられたのが "リソース管理オブジェクト" です.

でも,リソース漏れって例外とかのことも考えるとすごい厄介ですよね.例えば,下記のコードだとnewしたところからdeleteのところまでで例外とかが起こってしまうと,リソース漏れてしまうんですよね...でも,下記のケースでもリソース管理オブジェクトを適切に使っておけば,確実にリソース開放してくれます.

Resouce *a = new Resource();

.....

func_that_might_cause_execption();

....

delete a;

2.shared_ptr, auto_ptr, unique_ptr

Effective C++では,auto_ptr, shared_ptrというものが扱われてますが,どうやら,auto_ptrはdeprecatedになって,今後は同じような場面ではunique_ptrを使わないといけないみたい.

2.1 auto_ptrのサンプル

deprecatedのauto_ptrですが,定義通り別の auto_ptr に代入すると,元のauto_ptrが指すポインタはnullになります.

  // スコープ内でauto_ptrを宣言.
  {
    std::auto_ptr<Resource> pRsrc1(new Resource());
    std::auto_ptr<Resource> pRsrc2;
    pRsrc1->say();
    
    // auto_ptrはポインタをシェアできないので,代入後はもとのauto_ptrがNULLを指す.
    pRsrc2 = pRsrc1;
    std::cout << pRsrc1.get() << std::endl;
    std::cout << pRsrc2.get() << std::endl;
    pRsrc2->say();
  }

実行結果

Resource()
Resouce::say()
0             <- 代入後は元のauto_ptrはnullを指す.
0x16c1030        <- 代入後は代入された側のauto_ptrはReasourceオブジェクトへのポインタを指す.
Resouce::say()
~Resource()

2.2 unique_ptr

deprecatedのauto_ptrの代わりにできたunique_ptr.そもそも代入演算子が定義されてないらしく,"a = b" という書き方をするとコンパイルエラーになります.

  // auto_ptrはdeprecatedらしく,推奨はunique_ptrらしい.unique_ptrの場合,もはや代入演算子を使った代入ができない.コンパイルエラーになる.
  {
    std::unique_ptr<Resource> pRsrc1(new Resource());
    std::unique_ptr<Resource> pRsrc2;
    pRsrc1->say();
    
    // unique_ptrはポインタをシェアできず,代入演算子の使用も不可.
    //pRsrc2 = pRsrc1;
    std::cout << pRsrc1.get() << std::endl;
    //std::cout << pRsrc2.get() << std::endl;
    pRsrc1->say();
    //pRsrc2->say();
  }

実行結果

Resource()
Resouce::say()
0x16c1030      <- unique_prtはそもそも代入できなかった(コンパイルエラー).
Resouce::say()
~Resource()

2.3 shared_ptr

ポインタシェアできる shared_ptr.当然のことながら,シェアしても元のリソースのデストラクタは一回しか呼ばれません.

  // スコープ内でshared_ptrを宣言.
  {
    std::shared_ptr<Resource> pRsrc1(new Resource());
    std::shared_ptr<Resource> pRsrc2;
    pRsrc1->say();
    
    // shared_ptrはポインタをシェアできるので,代入後も2つのshared_ptrが指すアドレスは同じ.
    pRsrc2 = pRsrc1;
    std::cout << pRsrc1 << std::endl;
    std::cout << pRsrc2 << std::endl;
    pRsrc1->say();
    pRsrc2->say();
  }

実行結果

Resource()
Resouce::say()
0x16c1030         <- 2つとも同じアドレスを指していることがわかる.
0x16c1030         <- 2つとも同じアドレスを指していることがわかる. 
Resouce::say()
Resouce::say()
~Resource()       <- ただし,デストラクタは一回だけ.

3.実装

ということで,本日のサンプルコードです.

#include <iostream>
#include <memory>
#include <string>

class Resource {

public:
  Resource() { std::cout << "Resource()" << std::endl; }
  ~Resource() { std::cout << "~Resource()" << std::endl; }
  void say() { std::cout << "Resouce::say()" << std::endl; }
};

int main(int, char**) {

  std::cout << std::endl << std::endl;

  // スコープ内でauto_ptrを宣言.
  {
    std::auto_ptr<Resource> pRsrc1(new Resource());
    std::auto_ptr<Resource> pRsrc2;
    pRsrc1->say();
    
    // auto_ptrはポインタをシェアできないので,代入後はもとのauto_ptrがNULLを指す.
    pRsrc2 = pRsrc1;
    std::cout << pRsrc1.get() << std::endl;
    std::cout << pRsrc2.get() << std::endl;
    pRsrc1->say();
    pRsrc2->say();
  }

  std::cout << std::endl << std::endl;

  // auto_ptrはdeprecatedらしく,推奨はunique_ptrらしい.unique_ptrの場合,もはや代入演算子を使った代入ができない.
  {
    std::unique_ptr<Resource> pRsrc1(new Resource());
    std::unique_ptr<Resource> pRsrc2;
    pRsrc1->say();
    
    // unique_ptrはポインタをシェアできず,代入演算子の使用も不可.
    //pRsrc2 = pRsrc1;
    std::cout << pRsrc1.get() << std::endl;
    //std::cout << pRsrc2.get() << std::endl;
    pRsrc1->say();
    //pRsrc2->say();
  }

  std::cout << std::endl << std::endl;

  // スコープ内でshared_ptrを宣言.
  {
    std::shared_ptr<Resource> pRsrc1(new Resource());
    std::shared_ptr<Resource> pRsrc2;
    pRsrc1->say();
    
    // shared_ptrはポインタをシェアできるので,代入後も2つのshared_ptrが指すアドレスは同じ.
    pRsrc2 = pRsrc1;
    std::cout << pRsrc1 << std::endl;
    std::cout << pRsrc2 << std::endl;
    pRsrc1->say();
    pRsrc2->say();
  }

  std::cout << "End of program." << std::endl;
  return 0;
}