🥏

リファクタリングするならもっとモダンに

に公開1

私がプログラミングで一番好きなのは、新しいコードを書くのではなく、既存のコードを動作を保ったまま改善することです。これはリファクタリングと言います。
新しい機能を実装する時に、まずは安直にでも良いので実装してみて、期待通りの動作になるようにしてからリファクタリングする、ような場合もあったりしますよね。

なので、今回は以前紹介した以下のfinallyのコードをリファクタリングしてみました。

template <class F>
class final_action
{
public:
    explicit final_action(const F& ff) noexcept : f{ff} {}
    explicit final_action(F&& ff) noexcept : f{std::move(ff)} {}

    ~final_action() noexcept { if (invoke) f(); }

    final_action(final_action&& other) noexcept
        : f(std::move(other.f)), invoke(std::exchange(other.invoke, false))
    {}

    final_action(const final_action&)   = delete;
    void operator=(const final_action&) = delete;
    void operator=(final_action&&)      = delete;

private:
    F f;
    bool invoke = true;
};

template <class F>
[[nodiscard]] auto finally(F&& f) noexcept
{
    return final_action<std::decay_t<F>>{std::forward<F>(f)};
}

final_actionは、テンプレートクラスになっていて、何かコールできるオブジェクトをコンストラクタに渡すと、デストラクタの中でコールしてくれます。
そうすることで、エラーや例外でスコープを抜けても、意図してearly return(早期リターン)しても、スコープ内のコードが正常に最後まで実行されても、一番最後に指定されたコードを絶対に実行してくれます。
finallyfinal_actionを作ってくれるユーティリティ関数です。

finallyは元々 C++ の設計/実装者 Bjarne Stroustrup が書いた C++ Core Guidelines の E.19 に提案されました。

C++ Core Guidelines で提案されている全ユーティリティクラスと関数は GSL (Guidelines Support Library) という名前でマイクロソフトによって実装されています。 こちらもマイクロソフトの実装になります。

実装としてもちろんとてもよくできていますが、個人的に問題が4点あります。

  1. finallyはあんまり意味がないです。final_actionを作りやすくするはずですが、同じ引数のため実装的にあんまり変わらないです。

    auto f = finally([] { printf("Hello World!\n"); });
    final_action f([] { printf("Hello World!\n"); });
    

    むしろfinal_actionを直接作る方が、そのスコープから出ると破棄されるオブジェクトのインスタンスを作っている感じがあって、個人的に分かりやすいです。

  2. moveコンストラクタを入れたせいで、必要以上に複雑になっています。個人的にはfinal_actionのインスタンスはmoveさせる必要性はないです。moveできるってことは、そのインスタンスをそのスコープから移動させられるようになるってことで、むしろmoveできなくしたいです。特定のスコープの中で作ったら、そのスコープから出れないオブジェクトにしたいです。

  3. そもそも渡されたオブジェクトの型はコールできる型なのかというチェックが行われていないです。もちろん今の状態でも、コールできない型のオブジェクトを渡したらエラーになりますが、コード上ではもっと分かりやすくしたいです。今だと、class Fとだけ書いてあって、コールできるオブジェクトでないと駄目というのは分かりづらいです。

  4. コンストラクタの引数には関数へのポインタも渡せますが、デストラクタの中でコールする前にNULLチェックが行われていないです。

Microsoft の実装が間違っているとは言っていないです。これは既存のコードを自分のプログラムに合わせてカスタマイズするという意味でのリファクタリングになります。

これらの問題に対応したリファクタリング後の実装は以下になります。

#include <concepts>
#include <functional>
#include <type_traits>
#include <utility>

template <typename T>
struct is_std_function : std::false_type {};

template <typename Ret, typename... Args>
struct is_std_function<std::function<Ret(Args...)>> : std::true_type {};

template <typename T>
constexpr bool is_std_function_v = is_std_function<T>::value;

template <std::invocable Callable>
class final_action
{
public:
    explicit final_action(const Callable& callable) noexcept // constructor
    : m_callable{callable}
    {}
    explicit final_action(Callable&& callable) noexcept // constructor
    : m_callable{std::move(callable)}
    {}

    ~final_action() noexcept // destructor
    {
        if constexpr (std::is_pointer_v<Callable> || is_std_function_v<Callable>)
        {
            if (!m_callable)
            {
                return;
            }
        }
        m_callable();
    }

    final_action(const final_action&)        = delete; // copy constructor
    final_action(final_action&& other)       = delete; // move constructor
    void operator=(const final_action&) = delete; // copy assignment
    void operator=(final_action&&)      = delete; // move assignment

private:
    Callable m_callable;
};

それぞれの問題を以下のように修正しました。

  1. finallyというユーティリティ関数を削除しました。
  2. moveコンストラクタを無効にして、invokeというメンバー変数を削除しました。
    1. マイクロソフトの実装では、オブジェクトをmoveした場合、渡された関数はmoveされた方のオブジェクトのデストラクタの中でコールされるため、move先のオブジェクトではもうコールされなくするためにinvokefalseにする必要がありました。moveコンストラクタを無効の場合は、invokeも不要になります。
  3. <class F><std::invocable Callable>に変えることで、コールできる型を渡さないといけないということが明白になります。
    1. でもstd::invocableは C++20 からしか使えないため、C++20 がまだ使えない環境では、せめて名前でも分かりやすくするために<class Callable>でもいいです。
  4. デストラクタの中でm_callableをコールする前に、ポインタ型の場合のみNULLチェックをして、NULLの場合はreturnします。

NULLチェックが一番難しかったです。
普通にNULLチェックをしようとすると、そもそもポインタ型でないタイプを渡す場合コンパイルエラーになります。
ポインタ型かはstd::is_pointer_vで判断できますが、コンパイル時にチェックしないと、やはりコンパイルエラーになってしまうため、constexprを付けました。これでコンパイル時にポインタ型かをチェックして、ポインタ型のオブジェクト場合のみNULLチェックをします。

でもこれでは足りないです。std::functionを渡した場合、空のstd::functionかどうか、つまり実際に関数が設定されているかどうかをチェックする時も、NULLチェックと同様にoperator bool()が使えますが、std::functionはポインタ型ではないため、std::is_pointer_vで弾かれてしまいます。標準ライブラリにはテンプレートの型がstd::functionかどうかをチェックできる関数は提供されていないため、自分でSFINAEを使ってそういった関数を実装するしかなかったです。それがis_std_function_vになります。これで、m_callableがポインタ型かstd::functionの場合のみ、NULLチェックを行うようにできました。

final_actionを実際に使う時は大体はラムダ式関数を渡すと思いますが、コールできるオブジェクトはoperator()が定義されているものすべてになるので、以下のように色々なパターンに対応しないといけないです。

void callable1() {}

int main() {
    auto callable2([]{});
    std::function<void()> callable3;

    struct CallableStruct
    {
        void operator()() {}
    };
    CallableStruct callable4;

    final_action final_free(&callable1); // 関数ポインタ
    final_action final_free(callable2);  // ラムダ式関数
    final_action final_free(callable3);  // std::function
    final_action final_free(callable4);  // operator() がある構造体

    return 0;
}

もし、毎回final_actionのインスタンスに名前を付けるのが嫌だったら、以下のマクロを使えば、自動的にユニークな名前を作ってくれます。

#define NAME2(A,B)           NAME2_HELPER(A,B)
#define NAME2_HELPER(A,B)    A ## B
#define UNIQUE_NAME(PREFIX)  NAME2(NAME2(PREFIX,_),__LINE__)

#define finally              final_action UNIQUE_NAME(defer)

こちらを使うと、もっと使いやすくなると思います。

void f(int n)
{
   void* p = malloc(n);
   finally([p] { free(p); });
// ...
}

リファクタリングは本当に永遠にできてしまいますね。新しい C++ のバージョンが使えるようになった時も、できることが増えるため、また既存のコードをリファクタリングしたくなりますね。
でもリファクタリングする前に機能をできるだけカバーするユニットテストを用意しないと、不具合を導入してしまう恐れがあるので、ご注意ください。


|cpp記事一覧へのリンク|

Discussion

dameyodamedamedameyodamedame

2025年12月21日現在の件のガイドラインE.19を引用すると、

E.19: Use a final_action object to express cleanup if no suitable resource handle is available
Reason

finally from the GSL is less verbose and harder to get wrong than try/catch. Example

void f(int n)
{
    void* p = malloc(n);
    auto _ = gsl::finally([p] { free(p); });
    // ...
}

Note finally is not as messy as try/catch, but it is still ad-hoc. Prefer proper resource management objects. Consider finally a last resort.

Note Use of finally is a systematic and reasonably clean alternative to the old goto exit; technique for dealing with cleanup where resource management is not systematic. Enforcement

Heuristic: Detect goto exit;

と書かれています。ここで注意すべきは

Note finally is not as messy as try/catch, but it is still ad-hoc. Prefer proper resource management objects. Consider finally a last resort.

という部分です。意訳すると以下な感じです。

注意 finallyはtry/catchほど複雑ではありませんが、それでもアドホックです。適切なリソース管理オブジェクトを使用することをお勧めします。finallyは最後の手段と考えてください。

つまり、やむを得ない場合を除いて「適切なリソース管理オブジェクト」(RAIIなど)が推奨なわけです。

そんなところをリファクタリングをする前に、またそれがモダンかどうかを考える前に、まずは正しく理解してください。