リファクタリングするならもっとモダンに
私がプログラミングで一番好きなのは、新しいコードを書くのではなく、既存のコードを動作を保ったまま改善することです。これはリファクタリングと言います。
新しい機能を実装する時に、まずは安直にでも良いので実装してみて、期待通りの動作になるようにしてからリファクタリングする、ような場合もあったりしますよね。
なので、今回は以前紹介した以下の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(早期リターン)しても、スコープ内のコードが正常に最後まで実行されても、一番最後に指定されたコードを絶対に実行してくれます。
finallyはfinal_actionを作ってくれるユーティリティ関数です。
finallyは元々 C++ の設計/実装者 Bjarne Stroustrup が書いた C++ Core Guidelines の E.19 に提案されました。
C++ Core Guidelines で提案されている全ユーティリティクラスと関数は GSL (Guidelines Support Library) という名前でマイクロソフトによって実装されています。 こちらもマイクロソフトの実装になります。
実装としてもちろんとてもよくできていますが、個人的に問題が4点あります。
-
finallyはあんまり意味がないです。final_actionを作りやすくするはずですが、同じ引数のため実装的にあんまり変わらないです。auto f = finally([] { printf("Hello World!\n"); }); final_action f([] { printf("Hello World!\n"); });むしろ
final_actionを直接作る方が、そのスコープから出ると破棄されるオブジェクトのインスタンスを作っている感じがあって、個人的に分かりやすいです。 -
moveコンストラクタを入れたせいで、必要以上に複雑になっています。個人的にはfinal_actionのインスタンスはmoveさせる必要性はないです。moveできるってことは、そのインスタンスをそのスコープから移動させられるようになるってことで、むしろmoveできなくしたいです。特定のスコープの中で作ったら、そのスコープから出れないオブジェクトにしたいです。 -
そもそも渡されたオブジェクトの型はコールできる型なのかというチェックが行われていないです。もちろん今の状態でも、コールできない型のオブジェクトを渡したらエラーになりますが、コード上ではもっと分かりやすくしたいです。今だと、
class Fとだけ書いてあって、コールできるオブジェクトでないと駄目というのは分かりづらいです。 -
コンストラクタの引数には関数へのポインタも渡せますが、デストラクタの中でコールする前に
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;
};
それぞれの問題を以下のように修正しました。
-
finallyというユーティリティ関数を削除しました。 -
moveコンストラクタを無効にして、invokeというメンバー変数を削除しました。- マイクロソフトの実装では、オブジェクトを
moveした場合、渡された関数はmoveされた方のオブジェクトのデストラクタの中でコールされるため、move先のオブジェクトではもうコールされなくするためにinvokeをfalseにする必要がありました。moveコンストラクタを無効の場合は、invokeも不要になります。
- マイクロソフトの実装では、オブジェクトを
-
<class F>を<std::invocable Callable>に変えることで、コールできる型を渡さないといけないということが明白になります。- でも
std::invocableは C++20 からしか使えないため、C++20 がまだ使えない環境では、せめて名前でも分かりやすくするために<class Callable>でもいいです。
- でも
- デストラクタの中で
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++ のバージョンが使えるようになった時も、できることが増えるため、また既存のコードをリファクタリングしたくなりますね。
でもリファクタリングする前に機能をできるだけカバーするユニットテストを用意しないと、不具合を導入してしまう恐れがあるので、ご注意ください。
Discussion
2025年12月21日現在の件のガイドラインE.19を引用すると、
と書かれています。ここで注意すべきは
という部分です。意訳すると以下な感じです。
つまり、やむを得ない場合を除いて「適切なリソース管理オブジェクト」(RAIIなど)が推奨なわけです。
そんなところをリファクタリングをする前に、またそれがモダンかどうかを考える前に、まずは正しく理解してください。