Skip to content

Latest commit

 

History

History
195 lines (146 loc) · 6.46 KB

scope_guard.md

File metadata and controls

195 lines (146 loc) · 6.46 KB

スコープを抜ける際に実行されるブロック

Boost.ScopeExit

スコープを抜ける際に、リソースを確実に解放するのはC++のRAIIという手法で行われる。しかし、RAIIはクラスと、そのデストラクタを書くことによってリソースを解放するため、関数ローカルにおいて即興で必要となる場合にはコード量が多くなってしまう。

Boost C++ Librariesでは、関数を抜ける際に実行される式、またはブロックを定義する方法をいくつか提供している。

インデックス

Boost.ScopeExitは、関数のスコープを抜ける際に実行されるブロックを定義するためのBOOST_SCOPE_EXITマクロを提供する。

以下がその基本的な使い方である:

#include <iostream>
#include <boost/scope_exit.hpp>

struct X {
    int value;

    void foo()
    {
        value = 0;

        BOOST_SCOPE_EXIT((&value)) {
            value = 2;
        } BOOST_SCOPE_EXIT_END

        value = 1;
    }
};

int main()
{
    X x;
    x.foo();

    std::cout << x.value << std::endl;
}

実行結果:

2

関数X::foo()の中では、関数の先頭でvalue0を代入し、関数を抜ける直前でvalue1を代入している。

そして、BOOST_SCOPE_EXITマクロで定義されたブロックの中でvalue2が代入されているが、このブロックがX::foo()を抜けるタイミングで呼び出されることで、最終的にvalueの値が2になっている。

Boost.ScopeExitは 変数のキャプチャ という機能を持っており、

&value

という表記によって、変数valueへの参照をScope Exit構文の中で使用できるようにしている。

value

と書いた場合には、変数valueのコピーをScope Exit構文の中で使用できるようになる。

また、以下のように、変数をカッコで囲んで連続で記述することにより、複数の変数をキャプチャすることができる。

BOOST_SCOPE_EXIT((&x)(&y)) {
    x = x + 1;
    y = y + 1;
} BOOST_SCOPE_EXIT_END

なお、Boost-1.50以降ではラムダ式を用いたC++11版のBOOST_SCOPE_EXIT_ALLマクロが追加され、次のように簡潔なコードも可能になった。

BOOST_SCOPE_EXIT_ALL(&x, &y) {
    x = x + 1;
    y = y + 1;
};

BOOST_SCOPE_EXIT_ALLでは引数をカンマで区切り、それぞれに=または&により値キャプチャーと参照キャプチャーを定義でき、BOOST_SCOPE_EXIT_ENDに相当する終端マクロは不要になった。ただし、スコープ定義の終わりにはステートメント終端のセミコロンが必要になっている点に注意されたい。

BOOST_SCOPE_EXITBOOST_SCOPE_EXIT_TPLの内部実装をBOOST_SCOPE_EXIT_ALLと同様にラムダ式を使うバージョンに切り替えるためのBOOST_SCOPE_EXIT_CONFIG_USE_LAMBDASマクロも用意されている。

Boost.ScopeExitの使いどころ

Boost ScopeExitは主にメンバ変数に対する関数内でのコミット/ロールバックを目的に使用されることが多い。

たとえば、ボタンクラスを作成することを考える。

ボタンは、「押した」「離した」という状態をボタン自身に伝える機能を持ち、内部で通常状態と押下状態の画像を切り替えることができる。

class Button {
public:
    void down();
    void up();

    bool is_down() const;
    bool in_rect(const Point& p) const;
};

そして、ボタンをメンバ変数として持つ画面クラスが、画面のある位置をクリックした場合に呼ばれるハンドラを持っているとしよう。

以下のように書くことでボタンの画像切り替えロジックが書ける。

class View {
    Button back_button_;
public:
    void on_click_down(const Point& p)
    {
        if (back_button_.in_rect(p)) {
            back_button_.down(); // 押下状態の画像に切替える
        }
    }

    void on_click_up(const Point& p)
    {
        if (back_button_.is_down() &&
            back_button_.in_rect(p)) {
            back_button_.up(); // 通常状態の画像に切り替える
            on_back_button();
        }
    }

    // 戻るボタンが押された
    void on_back_button()
    {
        ...
    }
};

ここでは、on_click_up()の中で「押されていたら離して処理する」ということをしている。

このプログラムが問題になるのは、ボタンが増えたときや、途中でreturnする必要が出てきた場合である。押されている状態からでなければ離すことはできないので、関数の始めに離すことはできず、途中でreturnされることを考えると関数の最後で離すこともできない。

そういったときに、Boost.ScopeExitを使用することで、関数のスコープを抜けた際に、全てのボタンを確実に離すことができる。

class View {
    Button back_button_;
    Button next_button_;
public:
    void on_click_down(const Point& p)
    {
        if (back_button_.in_rect(p)) {
            back_button_.down(); // 押下状態の画像に切替える
        }

        if (next_button_.in_rect(p)) {
            next_button_.down();
        }
    }

    void on_click_up(const Point& p)
    {
        BOOST_SCOPE_EXIT((&back_button_)(&next_button_)) {
            // スコープを抜ける際に全てのボタンを離す
            back_button_.up();
            next_button_.up();
        } BOOST_SCOPE_EXIT_END

        if (back_button_.is_down() &&
            back_button_.in_rect(p)) {
            on_back_button();
        }

        if (next_button_.is_down() &&
            next_button_.in_rect(p)) {
            on_next_button();
        }
    }

    // 戻るボタンが押された
    void on_back_button()
    {
        ...
    }

    // 次へボタンが押された
    void on_next_button()
    {
        ...
    }
};