「unique_ptrにポインタ以外のものを持たせるとき」で話したことと話さなかったこと

歌舞伎座.tech#8「C++初心者会」で「unique_ptrにポインタ以外のものを持たせるとき」というタイトルで発表したが、この記事ではそのまとめと、追加の話題について述べる。
発表資料はここにある

TL;DR

std::unique_ptrはハンドルがポインタライクな型のリソースでしか使用できない。それに対して、C++標準委員会のペーパーN4189で提案されているunique_resourceを使えば、ポインタ以外のハンドルで扱われ、排他的に所有されるリソースを自動管理できる。

実際に使用できるunique_resourceの実装はここにある

unique_resourceは普通、カスタムデリータを設定して使用するが、デリータオブジェクトのコピーと実行が強い保証を満たしていない場合は例外安全性を担保するのが困難、もしくはほぼ不可能である。

デリータ型がムーブ代入不可能の場合(ラムダであるなど)はunique_resourceはムーブ代入不可能になる。

「unique_ptrにポインタ以外のものを持たせるとき」で話したこと

動的にメモリ確保したオブジェクトは最終的に解放する必要がある。

auto* widget = new Widget();
// ....
// widgetを使う何らかの処理
// ....
delete widget; // きちんと解放する

解放しなかった場合はメモリリークとなり問題が発生する恐れがある。簡単なコードでは解放忘れしないように注意すれば済むかもしれないが、コードが複雑になるにつれて解放処理を行うべきコードパスの把握は指数的に困難になる。

きちんと然るべきタイミングでリソースが解放されるよう処理を自動化できれば便利だ。std::unique_ptrはまさにそのためのものである。

auto* widget = std::unique_ptr<Widget>(new Widget());
// ....
// widgetを使う何らかの処理
// ....
// スコープの終わりでwidgetは自動で解放される。

話が少し逸れるが、C++14ではunique_ptrのファクトリ関数であるmake_unique関数が標準ライブラリに入ったため、C++14において上例の用法ではnewのような忌まわしいキーワードを打ち込む必要はない。

auto widget = std::make_unique_ptr<Widget>(); // newなし
// ....
// widgetを使う何らかの処理
// ....
// やはりスコープの終わりでwidgetは自動で解放される。

さて、std::unique_ptrではメモリリソースの解放時に呼び出される関数(デリータ)を設定することができる。

// Widgetのファクトリ関数
decltype(auto) make_widget() {
  auto deleter = [](Widget* wp) {
    std::cout << "deleter is called" << std::endl;
    delete wp;// きちんとdeleteしておく
  };
    // デリータを設定する場合はmake_unique関数は使えない
  return std::unique_ptr<Widget, decltype(deleter)>(
      new Widget(), std::move(deleter));
}
int main() {
  auto wp = make_widget();
}

出力

deleter is called

このようなユーザによるカスタムデリータはメモリの解放以外に後処理を行わなければならない場合に用いられることが想定されているが、ここでちょっとした思いつきが出てくる。デリータがカスタマイズできるなら、メモリリソース以外のリソースの管理にも使えるのではないだろうか?

namespace others /*他人のAPI*/ {
  int GetHandle() {/*略*/} // リソースハンドルはint型
  void ReleaseHandle(int handle) {/*略*/}
}
decltype(auto) make_unique_handle() {
  auto deleter = [](int handle) {
    others::ReleaseHandle(handle); // deleteの代わりに解放関数を呼ぶ
  };
  return std::unique_ptr<int, decltype(deleter)>(
    others::GetHandle(), std::move(deleter));
}
int main() {
  auto h = make_unique_handle();
}

上例のコードはコンパイルできない。GetHandle関数の返り値型がポインタではないというようなエラーが出る(int*を渡すべきところを単にintを渡しているだけなので当たり前の話だが)。

実はunique_ptrでラップする型はデリータのメンバ型pointerをカスタマイズすることで設定することができる。

namespace others {/*略*/}
// ラムダではメンバ型が定義できないので、昔ながらの関数オブジェクトクラスを使う
struct deleter {
  using pointer = int; // デフォルトではint*になるのを、intと指定
  void operator()(int handle) { others::ReleaseHandle(handle); }
};
decltype(auto) make_unique_handle() {
  return std::unique_ptr<int, deleter>(
    others::GetHandle(), deleter{}); 
}
int main() {
  auto h = make_unique_handle();
}

これで問題ないように思えるが、やはり上例もコンパイルできない。intとnullptrの比較ができないというようなエラーが出る。 unique_ptr用デリータのメンバ型pointerはnullptr_t型(nullptrの型)と比較可能であることが要件として定められているのだ。

ここで詰んでしまう。どうやってもintとnullptrの比較関数を定義できない。組み込み型とnullptrの比較は規格で禁止されているためである。

An operator function shall either be a non-static member function or be a non-member function that has at least one parameter whose type is a class, a reference to a class, an enumeration, or a reference to an enumeration. --N4527 13.5 6

リソースハンドルが組み込み型でない場合は比較関数を定義することは可能であるが、そもそもリソースハンドルをnullptrと比較することにどういう意味があるのだろう。

だめだ。認めるしかない。std::unique_ptrはポインタをハンドルとするリソースにしか使えないのである。 ポインタでないハンドルで扱うリソースをunique_ptrのように自動で管理するためには、自分で型を書くしかないのだろうか。

class unique_handle {
  void safe_release() {
    if(handle_ == others::NullHandle) { return; }
    others::ReleaseHandle(handle_);
    handle_ = others::NullHandle;
  }
  int handle_;
public:
  unique_handle() : handle_(others::GetHandle()) {}
  unique_handle(unique_handle&& rhs) : handle_(others::NullHandle) {
    std::swap(handle_, rhs.handle_);
  }
  decltype(auto) operator=(unique_handle&& rhs) {
    safe_release();
    std::swap(handle_, rhs.handle_);
    return *this;
  }
  ~unique_handle() {
    safe_release();
  }
  decltype(auto) get() const { return handle_; }
};

しかし自分でリソースごとにハンドル管理クラスを書くのは面倒な上に、以下の事柄に注意しなければならない。

実際にこれらの点に注意して独自のリソース管理クラスをかちっと書くのも悪くないだろうが、面倒なのは確かである。

少し整理しよう。今、我々に必要なのは、次に挙げる特徴を備えたリソース管理クラスである。

  • 任意の型のリソースハンドルに使える
  • スコープを抜けたら自動で指定したデリータを実行する
  • ムーブできる
  • 標準ライブラリ並に信頼できる

そんな都合のいいライブラリがどこかにないのだろうか。実は、あるのだ。

N4189 Generic Scope Guard RAII Wrapper for the Standard Library 「unique_resource」である。

unique_reourceとはC++標準化委員会のペーパーで次期C++標準ライブラリに加えることを提案されているライブラリであり、unique_ptrの一般化である。

unique_resourceを使えば先のリソース管理クラスは次のように書ける。

namespace others {/*略*/}
decltype(auto) make_unique_handle() {
  return make_unique_resource(
    others::GetHandle(), &others::ReleaseHandle);
}

至極簡単に書ける。そして次のように使える。

int main() {
  auto h1 = make_unique_handle();
  auto h2 = make_unique_handle();
  auto h3 = make_unique_handle();
  h2 = std::move(h3); // move可能 h2の元々のハンドルはリリース
  auto h4 = make_unique_handle();
  std::cout << h4.get() << std::endl; // 生のハンドルにアクセス
  h4.reset(); // 明示的にハンドルをリリース
}

素晴らしい。

歌舞伎座での発表時は、unique_resourceはN4189にC++14の実装例があるため、そこからコピペすれば今すぐ使うことができるというふうに紹介したが、ライセンスの微妙な問題があった。そこでN4189の提起者であり、実装例コードの作成者であるPeter Sommerlad氏とAndrew L.Sandoval氏に連絡を取ったところ、Boost Software License 1.0で公開する許可を快くいただけたため、ここで公開するGCCのためのワークアラウンドを追加し、C++11でも動作するようにコードの部分的な書き換え等も行っているので、直接コピペしたものより使いやすいだろう。

「unique_ptrにポインタ以外のものを持たせるとき」で話さなかったこと

デリータを指定するときの例外安全性

unique_ptrやunique_resourceのようなリソース管理クラスはRAIIラッパと呼ばれる。リソース漏れを防ぐために、RAIIラッパを使うというのは正当なやり方であるが、それだけでは不十分である。ただ使うだけではなく、正しく使わねばならない。

次の例ではunique_ptrにデリータとして、デリータオブジェクトdeleterを渡している。

auto w = std::unique_ptr(new Widget(), deleter);

実を言うと、std::unique_ptrを使っているにもかかわらず、このコード片を見ただけではリソース漏れがないとは言い切れない。デリータ型のコピーコンストラクタが例外を発生させる恐れがあるためである。

C++では関数呼び出しにおける引数の評価順は定められていない。言語処理系の実装者が自由に決めて良いのだ。

When a function is called, each parameter (8.3.5) shall be initialized (8.5, 12.8, 12.1) with its corresponding argument. [ Note: Such initializations are indeterminately sequenced with respect to each other (1.9) — end note ] --N4527 5.2.2 4

仮に、以下のような順番で評価されたとしよう。

  1. newによるWidgetの生成
  2. デリータ仮引数のコピーコンストラクタ呼び出し

2でエラーが発生した場合、unique_ptrオブジェクトは生成されず、newによって生成されたWidgetオブジェクトはユーザの手の届かない場所へ消えていく。これは紛うことなきメモリリソース漏れである。

ではデリータ型のコピーコンストラクタが強い保証を備えていればよいのかと問われれば、話はそう単純ではない。 次の例ではdeleterをムーブしている。

auto w = std::unique_ptr(new Widget(), std::move(deleter));

上例の場合は、moveによりムーブコンストラクタが呼び出されるならばムーブコンストラクタが強い保証を備えていることが望ましい。 他にも次のようにコンストラクタを呼び出して一時オブジェクトを渡す場合も考えてみよう。

auto w = std::unique_ptr(new Widget(), deleter_t());

上例の場合は、deleter_t型のコピーコンストラクタが(ムーブによるコピーが発生するならムーブコンストラクタが)強い保証を備えていることが望ましいことに加え、deleter_t型のコンストラクタ(今回はデフォルトコンストラクタ)呼び出しで例外が発生する恐れもあるため、deleter_t型のコンストラクタも強い保証を備えていることが望ましい。

これらが示すように、特定の関数が強い保証を備えていればよいという話ではないのである。指定の仕方にも関わってくるのだ。unique_ptrでデリータを指定する場合には例外が一切発生しないことが望ましい。例外が発生する恐れがある場合は、リソース漏れを防ぐために例外に対処する煩わしいtry-catchの処理を書かなければならないだろう。

unique_resourceでも同じ問題がある。 unique_ptrと違い、unique_ptrではリソースハンドルが必ずしも組み込み型(ポインタ型)でないためにハンドルのコンストラクタやコピーコンストラクタが例外を発生させる恐れもある(一方、組み込み型のコピーはエラーを発生させることはない)が、その場合はまだハンドルオブジェクトの生存期間が始まっていないため、std::unique_resourceの例外安全性とは関係がない(ハンドル型の問題である)。しかし、unique_ptrではデリータの設定が任意であるのに対して、unique_resourceでは必須であるため、やはりデリータのコピーコンストラクタの例外安全性には常に十分注意しなければならない。

デリータの実行の例外安全性

一般に、デストラクタで例外が発生しないことが保証できない場合、プログラムを例外安全に動作させることはほぼ不可能である。 ところで、unique_resourceのデリータはデストラクタで実行される。 ゆえにunique_resourceのデリータの実行は強い保証を備えているべきである。

実際に、unique_ptrのデリータの要件として、その実行が例外を発生させないことが要求されている。

デリータとしてラムダを用いた場合の制限

ラムダによって生成されたクロージャオブジェクトはコピー代入演算子がdeleteされていることが規格で定められている。

The closure type associated with a lambda-expression has no default constructor and a deleted copy assignment operator. It has a defaulted copy constructor and a defaulted move constructor (12.8). [ Note: These special member functions are implicitly defined as usual, and might therefore be defined as deleted. — endnote ] --N4527 5.1.2 20

コピー代入演算子がdeleteされていることにより、コンパイラによるムーブ代入演算子の暗黙の生成が抑制される。unique_resourceオブジェクトのムーブ代入はデリータのムーブ代入を含むため、デリータがラムダによって生成されたクロージャオブジェクトだった場合、unique_resourceオブジェクトのムーブ代入を行うコードはコンパイルできない(一方、ラムダによって生成されたクロージャオブジェクトのムーブコンストラクタコンパイラにより暗黙に生成されるため、デリータがラムダによって生成されたクロージャオブジェクトであるunique_resourceオブジェクトのムーブコンストラクトは可能である)。

decltype(auto) make_unique_handle_lambda() {
  return unique_resource(others::GenHandle(),
    [](auto const& handle){ others::ReleaseHandle(handle); });
}
decltype(auto) make_unique_handle_funptr() {
  return unique_resource(others::GenHandle(), &others::ReleaseHandle);
}
int main() {
  {
    auto h1 = make_unique_handle_lambda();
    auto h2 = std::move(h1); // move construct OK
    h1 = std::move(h2); // move assignment NG
  }
  {
    auto h1 = make_unique_handle_funptr();
    auto h2 = std::move(h1); // move construct OK
    h1 = std::move(h2); // move assignment OK
  }
}

現在clangではデリータがラムダによって生成されたクロージャオブジェクトであるunique_resourceオブジェクトのムーブ代入を行うコードのコンパイルが可能であるバグがある。うっかり規格に準拠していないコードのコンパイルが通ってしまう恐れがあるため、コンパイルにclangを使っている、かつ、デリータにラムダを用いる場合は、GCCなど別のコンパイラでコードをダブルチェックするなどして注意すべきであろう。

個人的には、デリータにはラムダを用いるのではなく、関数ポインタか、自分で書いた関数オブジェクトを用いるのが良いと思う。

デリータとしてstd::functionを用いる場合の注意点

リソースの種類は同じでも、異なるデリータで後処理したい場合がある。しかしunique_resourceのデリータ型はunique_resourceの型の一部なので、デリータの型が異なればunique_resourceの実体化した型は異なる。そこで、std::functionをデリータの型として設定し、同じunique_resourceの実体化した型でありながら、オブジェクトごとに異なるデリータで処理できるようにすることを考える。

当然、先に挙げたデリータの設定の例外安全性は考慮する必要がある。特にstd::functionの場合は、std::functionのムーブコンストラクトがnoexceptでないため、デリータの設定でstd::functionオブジェクトをムーブするのは避けたほうがよいだろう。

コピーコンストラクトについては内部に保持している関数オブジェクトの動的なコピーが発生するため、その関数オブジェクトのコピーコンストラクタは強い保証を備えていることが望ましい。動的なコピーなので、std::bad_alloc例外が発生する恐れもあるが、どちらにせよstd::bad_alloc例外が発生するような状況でできることはあまりないため、それについてはそこまで気にする必要はないだろう。

std::function型でない関数オブジェクトでstd::functionをコンストラクトする場合は、やはりその関数オブジェクトの動的なコピーが発生するため、std::functionをコピーコンストラクトする場合と同様の注意が必要である。ただし、関数ポインタやstd::reference_wrapperでラップされた関数オブジェクトでコンストラクトする場合は、例外が発生しないことが保証されていることは特筆すべきだろう。

{
  auto deleter = [](auto handle){ others::ReleaseHandle(handle); }; // deleterはコンパイラによって生成されたラムダ型
  auto h1 = unique_resource<handle_t, 
    std::function<void(handle_t)>>(others::GetHandle(), deleter); // 安全。このデリータはコピーコンストラクトで強い保証を備えており、このデリータでstd::functionをコンストラクトするのは安全(メモリが十分あれば)。
  auto h2 = unique_resource<handle_t, 
    std::function<void(handle_t)>>(others::GetHandle(), std::move(deleter)); // 安全。このデリータはムーブコンストラクトとコピーコンストラクトで強い保証を備えており、このデリータでstd::functionをコンストラクトするのは安全(メモリが十分あれば)。
}
{
  std::function<void(handle_t)> deleter = [](auto handle){ others::ReleaseHandle(handle); }; // deleterはstd::function型
  auto h1 = unique_resource<handle_t, 
    std::function<void(handle_t)>>(others::GetHandle(), deleter); // 安全。このデリータはコピーコンストラクトで強い保証を備えており、このデリータでstd::functionをコピーコンストラクトするのは安全(メモリが十分あれば)。
  auto h2 = unique_resource<handle_t, 
    std::function<void(handle_t)>>(others::GetHandle(), std::move(deleter)); // 危険。std::functionのムーブコンストラクタ呼び出しは例外を発生させるかもしれない。
}
{
  auto h = unique_resource<handle_t, 
    std::function<void(handle_t)>>(others::GetHandle(), &others::ReleaseHandle); // 安全。関数ポインタによるコンストラクト。
}
deleter_t unsafe_copy_ctor_deleter; // コピーコンストラクタで例外が発生する恐れのある関数オブジェクトのデリータ
{
  auto h = unique_resource<handle_t, 
    std::function<void(handle_t)>>(others::GetHandle(), unsafe_copy_ctor_deleter); // 危険。デリータのコピーで例外が発生する恐れがある。
  auto h = unique_resource<handle_t, 
    std::function<void(handle_t)>>(others::GetHandle(),
      std::reference_wrapper(unsafe_copy_ctor_deleter)); // 安全。std::reference_wrapperによるコンストラクト。
}

まとめ

  • ハンドルがポインタでない、排他的に所有されるリソースの自動管理のためにunique_resourceが提案されている。
  • unique_resourceはC++11/14で使うことができる
  • unique_resourceのデリータの指定では例外が一切発生しないことが望ましい。
  • unique_resourceのデリータの実行は強い保証を備えているべきである。
  • デリータにラムダを用いた場合、unique_resourceはムーブ代入できなくなる。
  • デリータにstd::functionを用いる場合もデリータ指定時の例外安全性については注意する必要がある。