2012/01/28

C++と仲直り計画: Cの構造体をスマートポインタで扱う (同時上映: ラムダ式もほんのちょっとかじる)

いきなりですが今日は boost::shared_ptr を構造体に対して用いるお話です。ここまで読んで、『何を今さら』と思った方はブラウザをそっと閉じると幸せになれますよ。

あと、今日の日記は解説というよりは備忘録的な色が強いので、僕と波長が合わないとなかなか内容に共感して頂けないかも知れません。そういう方もブラウザをそっt(ry




【だらだら C 言語からスマート C++ へ】

僕が C / C++ に苦手意識を抱く一つの理由がメモリ管理です。まず、動的に確保したメモリをどのタイミングで解放するか、あるいは解放のし忘れはないかを考えるのが煩わしい。次に、メモリ解放用のコードを書くのが面倒。

百聞は一見に何とやらで、メモリを動的に確保 / 解放するコードの例を以下に示します。
(ちなみに OpenCV を使っているのは個人的な趣味です)(ごめんね)

【OpenCV で画像領域を動的確保/解放(生ポインタ)】
#include<iostream>

#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>

#ifdef _DEBUG
#pragma comment(lib, "opencv_core231d.lib")
#pragma comment(lib, "opencv_highgui231d.lib")
#else
#pragma comment(lib, "opencv_core231.lib")
#pragma comment(lib, "opencv_highgui231.lib")
#endif

using namespace std;

int main() {
    // 400 × 300 で画像を作成
    IplImage* img = cvCreateImage(cvSize(400, 300), IPL_DEPTH_8U, 3);

    // img を表示
    cvShowImage("作った画像を見せびらかすウィンドウ", img);
    cv::waitKey(0);

    // img を解放
    cvReleaseImage(&img);
    img = NULL;
    
    cout << "メモリを適当に解放しました\n";
    return 0;
}

念のため、上のコードを簡単に説明すると、cvCreateImage で動的にメモリを確保して、cvReleaseImage で解放しています。これ、IplImage 型の変数が 1 つだけしかないからよいものの、変数の数だけ解放用コードが並ぶのはさぞ目障りなものでしょう。



ところが、boost というライブラリには、動的に生成されたオブジェクトを自動的にデストラクトしてくれる便利なスマートポインタが用意されているのです。

スマートポインタにもいくつか種類がありますが、今回は最も汎用性が高い(と個人的に考えている)boost::shared_ptr を使ってみましょう。

まず、boost::shared_ptr<iplimage> 用のカスタムデリータをファンクタの形で定義します(ここでは ImageReleaser と名付けた)。やっている事は、単に関数呼び出し演算子をオーバーロードして、cvReleaseImage を実行しているだけです。簡単ですね。

次に、このデリータオブジェクトを boost::shared_ptr<iplimage> のコンストラクタ(第2引数)に渡してやります。すると、たったこれだけで生成されたオブジェクトが自動解放されるようになります。やったね!

OpenCV で画像領域を動的確保/解放(スマートポインタ)】
#include<iostream>

#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>

#include<boost/shared_ptr.hpp>

#ifdef _DEBUG
#pragma comment(lib, "opencv_core231d.lib")
#pragma comment(lib, "opencv_highgui231d.lib")
#else
#pragma comment(lib, "opencv_core231.lib")
#pragma comment(lib, "opencv_highgui231.lib")
#endif

using namespace std;

// 解放用のファンクタ
class ImageReleaser {
public:
    void operator()(IplImage* img) {
        cvReleaseImage(&img);
        cout << "メモリを適当に解放しました\n";
    }
};


int main() {
    
    typedef boost::shared_ptr<IplImage> ImgPtr;
    
    // 400 × 300 で画像を作成
    ImgPtr img(cvCreateImage(cvSize(400, 300), IPL_DEPTH_8U, 3), 
               ImageReleaser());    // ←解放用ファンクタを指定

    // img を表示
    cvShowImage("作った画像を見せびらかすウィンドウ", img.get());
    cv::waitKey(0);

    // img は自動的に解放される
    return 0;
}

おー、できた。でもまだちょっとアレですね。

プログラム中で一度しか使われないデリータごときのためにわざわざファンクタを定義するのは大仰です。それに、極めて限定的な用途の小さなクラスがソース内に散らかるのはどうも汚く思えます。



ここで、先ほどの実装の改善すべき点を洗ってみましょう。

デリータは基本的に一度きりの使い捨てなので名前など要らないでしょうし、必要な時・必要な場所に実装を書けば「小さなクラスがソース内に散らかる」といった気持ち悪さもなくなります。

これを実現するのが boost::lambda です。そう、みんな大好きラムダ式のライブラリですね。ここではあまりマニアックな使い方はせず、単に邪魔なファンクタをこの世から消し去る目的のためだけに導入しました。

先ほどのソースを書き換えると以下のようになります。

OpenCV で画像領域を動的確保/解放(スマートポインタ + ラムダ式)】
#include<iostream>

#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>

#include<boost/shared_ptr.hpp>

#include<boost/lambda/bind.hpp>
#include<boost/lambda/lambda.hpp>

#ifdef _DEBUG
#pragma comment(lib, "opencv_core231d.lib")
#pragma comment(lib, "opencv_highgui231d.lib")
#else
#pragma comment(lib, "opencv_core231.lib")
#pragma comment(lib, "opencv_highgui231.lib")
#endif

using namespace std;

int main() {
    using namespace boost::lambda;
    typedef boost::shared_ptr<IplImage> ImgPtr;
    
    // 400 × 300 で画像を作成
    ImgPtr img(cvCreateImage(cvSize(400, 300), IPL_DEPTH_8U, 3),
        (bind(cvReleaseImage, &_1),
            cout << constant("メモリを適当に解放しました\n")));

    // img を表示
    cvShowImage("作った画像を見せびらかすウィンドウ", img.get());
    cv::waitKey(0);

    // img は自動的に解放される
    return 0;
}

この例のほかにも、ラムダ式を使えばソート関数にカスタムコンパレータを与えるコードをスマートに書けるでしょう(3次元 Delaunay 分割では boost レスで作ったので、わざわざコンパレータをファンクタにしていた)。

ちなみに OpenCV には cv::Ptr というすごい便利なスマートポインタが用意されています。というか、僕は boost を入れる前にもっぱらこちらを使っていました。

これは IplImage などのカスタムデリータが自動的に適用されるので、それを使えばひとまず上記の議論はことごとく不要になるのですが、たとえば悪名高い COM オブジェクトとか、C ベースで書かれた過去の重要な資産とかを安全に扱いたい場合には知っておくと幸せになれるような気がします。

0 件のコメント:

コメントを投稿

ひとことどうぞφ(・ω・,,)