C のライブラリを C++ で使う際、終了処理が必要なオブジェクトへのポインタを C++ のスマートポインタとして扱う場合、どの様に破棄させるのが良いのだろうか、という話。
C++ 向けに設計されたのオブジェクトというのは、大抵はデストラクタが定義されており、単に delete を呼び出す事で終了処理が行う設計になっているので、ポインタ経由で扱っても delete するだけで良い。 これに対して C 用に書かれた API は、コンストラクト関数があるオブジェクトのポインタを返し、それを使い終わると、デストラクト関数にそのポインタを渡す、といったスタイルが多い。 例えば ANSI C ライブラリでファイルを操作するときには、fopen 関数で FILE 型のオブジェクトへのポインタが返ってくるので、それを介して操作をし、使い終わったらそのポインタを fclose 関数に渡してオブジェクトの後片付けを行うといったスタイルである。 実例を挙げるまでも無いかも知れないが、以下の様なパターン。
FILE* fp = fopen("file_a.txt", "r");
...
fclose(fp);
この様な API を C++ で使う場合、直接ポインタを扱う事も当然可能ではあるものの、折角便利なスマートポインタが用意されているのだから、それを使いたくなるのは自然だし、寧ろそうすべきだ。 unique_ptr, shared_ptr のデフォルト deletor は、渡しておいたポインタに対して delete を行うだけで、C の関数では、new を使った初期化が行われる事を期待できないから、その様な恐らく malloc されたであろうヒープメモリを delete してしまうことになる。これは規格上未定義の動作を招く事になる。 勿論これらスマートポインタには、廃棄時のカスタマイズという場面も想定されており、deletor を指定することが出来るようになっている。 とはいえ、やり方をが幾つか思いつくので、どういうものを渡すのが良いのだろうかと迷った。この様な場合での場面での手法を幾つか挙げ、考えてみる。
// 検証に先立って、この様な wrapper を用意しておいて、deletor が呼ばれたことを確認できるようにしておく。
void wrap_fclose(FILE* fp) {
cout << "called wrap_fclose" << endl;
if (fp) fclose(fp);
}
ファンクタを実装する場合
unique_ptr のリファレンスを見て最初に頭に浮かんだ方法が、私の場合はこれだった。 最も素直な方法だと思う。安全確実だが、オブジェクトの型毎に定義が必要になるのが面倒だ。
// FILE* を受け取るファンクタを定義する。
struct FCloser {
void operator()(FILE* fp) {
wrap_fclose(fp);
}
};
// 使うとき
auto fp = unique_ptr<FILE, FCloser>{
fopen("data/input.txt", "r"),
};
構築時の記述はすっきりしてて好ましい。 だがやはり、仮引数の型と呼び出す関数が違うだけの、似たようなコードをいくつも実装するのは面倒だから、これはあとでクラステンプレートを考えよう。
std::default_delete を使う場合
スマートポインタの構築時に、deletor が指定されなかった場合、unique_ptr のデストラクタの中で、std::default_delete<> が呼び出される。 だから、この std::default_delete に対して、テンプレートの特殊化を利用すると、使っている型に対応した deletor が呼ばれるという寸法。
// std::default_delete<> を使う方法。
namespace std {
template <>
struct default_delete<FILE> {
void operator()(FILE* fp) const {
wrap_fclose(fp);
}
};
}
// 使うとき
auto fp = unique_ptr<FILE>{
fopen("data/input.txt", "r")
};
なるほど、その手が有ったか的な興味深い方法で、私もそういう手法は好む方だが、 結局特殊化の部分でファンクタを丸ごと実装する必要があるので、最初の方法と労力は変わってない。
しかも、Stackoverflow の記事の議論を見ていると、この方法はやめておいたほうが良さそうだ。 思ったように動作はするものの、一旦定義したら最早一切他の振る舞いをさせることが出来なくなる。 また、複数箇所でこの特殊化定義をしてしまった場合、リンカがエラーを出すだろう。 複数人で開発する場合に、default_delete の特殊化が行われている事を確実に周知できるだろうか? 等の懸念が残る。少なくとも積極的に使うべき理由は見当たらない。
std::function を使う場合
unique_ptr の第二テンプレート引数に std::function を指定する。
// Deletor
auto fp = unique_ptr<FILE, function<void(FILE*)>>{
fopen("data/input.txt", "r"),
&wrap_fclose
};
テンプレートの第二引数のデフォルト型が std::default_delete なので、適切な型を指定しないとコンパイルエラーになる。悪くないが、型の指定と関数ポインタが分離しているのを纏められないだろうか。
λ 式を使う場合
最も手軽に使える手法だが、複数箇所で使うならどこかで λ 式を変数に代入しておく事になるだろう。
// lambda
auto fp = unique_ptr<FILE, function<void(FILE*)>>{
fopen("data/input.txt", "r"),
[](FILE* p){ wrap_fclose(p); }
};
テンプレート実引数に std::function を渡して、コンストラクタの第二引数に λ 式を渡す事になる。この例の様な使い方なら、λ 式の代わりに関数ポインタを渡すだけでエエやんか、となる。 関数を呼び出すだけの、この様な場面だとコードが増えるだけに思える。だが、一回しか使わないなら最良の方法かなと思う。
decltype(失敗例)
ちょっと試してみようと思ったが失敗だった手法。 テンプレート第二引数に、関数ポインタで初期化した std::function の decltype を渡してみる。 その結果、コンパイルは出来るが、bad_function_call が throw される。勿論これでは使えない。 std::function はあくまで関数ポインタの型を表しているのでであって、関数ポインタそのものの型を表してくれるわけではないようだ。
// この方法は使えない
auto fp = unique_ptr<FILE, decltype(function<void(FILE*)>{&wrap_fclose})>{
fopen("data/input.txt", "r")
};
コンストラクタの第二引数に関数ポインタの実体を渡せば、当然ながら動作する。
ファンクタを template で実装しておく
以上がググッたり思いついたりしたものだが、やはり最初の方法を template にしておくのが良さそうに思う。 関数ポインタを呼び出すだけだが、どうせなら戻り値も仮引数も、template にしておいたら、別の用途にも流用出来るかもしれない。 ということで、書いたのが以下のクラステンプレート。
// 任意の型の関数ポインタを表わせるファンクタクラステンプレート
template <typename Signature>
struct FuncType;
template <typename ResultType, typename ...Types>
struct FuncType<ResultType(Types ...)> {
using pointer_type = ResultType (*)(Types ...);
template <pointer_type Func>
struct Pointer
{
constexpr Pointer() noexcept
{
}
constexpr ResultType operator()(Types ...args) const
noexcept(noexcept(Func(args...)))
{
return Func(args...);
}
};
template <pointer_type Func>
constexpr static auto InstanceOf = Pointer<Func>{};
FuncType() = delete;
};
任意の関数ポインタ型をテンプレート引数に渡す様にしたい。 戻り値は常に一つでよいが、引数リストは可変長としておく事になる。可変長表現そのものは C++11 から扱えるので問題ない。
さて、ここで肝心の関数ポインタを非型パラメタとして渡さなくてはならないが、可変長テンプレートの後ろにはもう引数を定義出来ない。これを解決する為には、このクラステンプレート内のコンテキストで、テンプレート型を参照した非型パラメタをテンプレート引数とするファンクタを定義すれば良さそうだ。それが上の内部クラステンプレート定義。 なお、std::function の関数的なシンタクスの型表現を真似る為に、定義自体は空で、テンプレートを特殊化した所に全ての定義を記述した。実装初期にこの内部クラスは std::function を継承するようにしていたが、そうするとコンストラクタに constexpr を指定できなかったので取りやめた。
一旦上のクラステンプレートを定義しておけば、任意の型のデストラクト関数を、テンプレートパラメタに渡すだけでよくなる。
// 関数の型と関数ポインタをテンプレート第二引数に渡す
auto fp = unique_ptr<FILE, FType::Pointer<wrap_fclose>>{
fopen("data/input.txt", "r")
};
下にある InstanceOf という変数テンプレートは、shared_ptr 構築時に渡すファンクタのインスタンスとして使用する(unique_ptr に使ってもいいけど)。 一時オブジェクトを作るのは一回だけで良いかなと思う。尤も、shared_ptr には第二引数に関数ポインタを渡すだけで良いので、もうちょっとメタプログラミング真っ只中の文脈でもないと、本末転倒になってしまっている。
// shared_ptr での利用
auto fp = shared_ptr<FILE>{
fopen("data/input.txt", "r"),
FuncType<void(FILE*)>::InstanceOf<wrap_fclose>
};
構築時の記法も、個人的にはそれなりに気に入っている。ただ、shared_ptr の方には、コンストラクタに関数オブジェクトを渡す方法しか用意されてないので、直交性がない点が気になる。unique_ptr の方も実装を見てみると、内部的には、デストラクト関数をメンバに持っていて、テンプレート型として、関数ポインタを渡しても、結局はファンクタを生成しているようだ。shared_ptr のインタフェイスの方が優れていると思うし、将来標準ライブラリが改良されるとしたら、この方法が deprecated とされるかも知れないな、と思うのは考えすぎかも知れないが、少し心配ではある。
なお、確認に使ったコンパイラは、clang3.7, g++5.2.1 のみ。-std=C++11 以降のオプションが必要。 変数テンプレートの方も利用するならば C++14 以降のオプションが必要。
0 件のコメント:
コメントを投稿
スパムフィルタが機能しないようなので、コメント不可にしました。
注: コメントを投稿できるのは、このブログのメンバーだけです。