🛀

安全に未初期化をケアするstd::optional

に公開

コードレビューで以下のようなコードを見かけました。

double x = 0.0;
if (condition)
{
   const int ret = doSomething1();
   if (ret == 0)
   {
      x = 10.0;
   }
}
if (x == 0.0)
{
   doSomething2(x);
}

気になった個所だけ抜粋してますので良く分からないコードになっていることは一旦置いておきましょう。

floatdouble型の変数は等価演算子で比べるのはお勧めされていですが、これは 0.0 と比較したいのではなく、どちらかというとxがまだ初期値のままかをチェックしています。でもコンパイラーの設定によって、0.0 との比較で警告がでますし、チェックの意図も分かりづらいです。

こういう場合は、私の一押しのstd::optionalを使うといいです。

#include <optional>

std::optional<double> x(std::nullopt);
if (condition)
{
   const int ret = doSomething1();
   if (ret == 0)
   {
      x = 10.0;
   }
}
if (x.has_value())
{
   doSomething2(x.value());
}

std::optionalはどの型の値も保存できるテンプレートクラスになっていて、デフォルト値がstd::nulloptという特別な値になっています。
上記の例では紹介のためわざわざstd::nulloptで初期化していますが、本当はその必要がないです。

std::optional型の変数は、value()という関数で値にアクセスできますが、その前に初期値のままか値が設定されているかは、has_value()という関数でチェックできます。
またstd::optionalクラスはboolへのキャスト演算子operator bool()も定義されているため、if文の条件としてそのまま書くこともできます。(if文は明示的なキャストになります)

https://en.cppreference.com/w/cpp/utility/optional/operator_bool

これで上記のコードをもう少し簡単に書けます。

#include <optional>

std::optional<double> x;
if (condition)
{
   const int ret = doSomething1();
   if (ret == 0)
   {
      x = 10.0;
   }
}
if (x)
{
   doSomething2(x.value());
}

これだと初期化のままかをチェックしているって一目で分かりますし、doubleの値を等価演算子で比較せずに済みます。

このコードをさらにリファクタリングしたい場合は、以下のようにラムダ式関数が使えます。

#include <optional>

auto get_x
( [] () -> std::optional<double>
   {
      if (condition && doSomething1() == 0)
         return 10.0;

      return {};
   }
);

const auto x = get_x();
if (x)
{
   doSomething2(x.value());
}

こうすると、xを一行で設定できて、さらにconstが付けられるようになりますね。


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

Discussion