【C++】std::optionalとは?|「値が無い」を安全に表現する仕組み

std optional thumbnail C++

「検索して見つからなかったとき、-1やnullptrを返すのってちょっと気持ち悪い」「std::optionalって結局何が嬉しいの?」「中身が無いときにアクセスするとどうなるの?」

こんな疑問はありませんか?

今回は、C++17で追加されたstd::optionalについて、実際にコンパイル・実行した結果を交えながら解説していきます。「値が無いかもしれない」をどう表現するのが安全なのか、実例で見ていきます。

std::optionalとは?

std::optionalとは、

「値があるかもしれないし、無いかもしれない」という状態そのものを型で表現できるテンプレートクラス

です。これまで「値が無い」ことを表すのに、ポインタならnullptr、整数なら-10のようなマジックナンバーを使うことがよくありました。しかし-1が本当に「無い」を意味するのか、本物の値の-1なのかはコードを読むだけでは区別できません。std::optionalは、この曖昧さを型レベルで解消します。

実装してみる

名前で敵のHPを検索する関数を、std::optional<int>を返す形で実装してみます。

struct Enemy {
    std::string name;
    int hp;
};

std::vector g_Enemies = {
    { "Slime", 30 },
    { "Goblin", 50 },
};

// 見つからないかもしれない検索結果を、ポインタやマジックナンバーではなくoptionalで返す
std::optional FindHpByName(const std::string& name) {
    for (auto& e : g_Enemies) {
        if (e.name == name) {
            return e.hp; // 見つかった: 値を返す
        }
    }
    return std::nullopt; // 見つからなかった: 「値が無い」を明示する
}

見つかったときは値をそのままreturnするだけ、見つからなかったときはstd::nulloptを返すだけです。呼び出し側でいくつかの使い方を試してみます。

auto hp1 = FindHpByName("Goblin");
printf("has_value() = %d\n", hp1.has_value());
printf("value()     = %d\n", hp1.value());

auto hp2 = FindHpByName("Dragon");
printf("has_value() = %d\n", hp2.has_value());
printf("value_or(-1) = %d\n", hp2.value_or(-1));

if (auto hp3 = FindHpByName("Slime")) {
    printf("Slimeのhpは%dです\n", *hp3);
}

実行結果です。

[1] 見つかる場合
    has_value() = 1
    value()     = 50

[2] 見つからない場合
    has_value() = 0
    value_or(-1) = -1

[3] if文で直接判定
    Slimeのhpは30です

has_value()で「中身があるか」を確認でき、value_or(デフォルト値)を使えば「無かったときの代わりの値」もその場で指定できます。さらにif (auto hp3 = ...)のようにoptional自体を条件式に書くだけでhas_value()相当の判定になり、中身は*hp3で取り出せます。マジックナンバーを覚えておく必要も、判定を書き忘れる心配も減ります。

【重要】中身が無いのにvalue()を呼ぶとどうなるか

ここまでは安全な使い方でしたが、has_value()で確認せずにvalue()を呼んでしまったらどうなるのか、実際に試してみます。

try {
    auto hp4 = FindHpByName("Dragon");
    int x = hp4.value(); // 中身が無いのにvalue()を呼ぶ
    printf("x = %d\n", x); // ここには来ない
} catch (const std::bad_optional_access& e) {
    printf("例外キャッチ: %s\n", e.what());
}
例外キャッチ: Bad optional access

未確認のままvalue()を呼ぶと、std::bad_optional_accessという例外が実際に投げられることが確認できました。ポインタのnullptrを確認せず参照外しするとクラッシュ(未定義動作)しますが、optionalvalue()はちゃんと例外という形でエラーを教えてくれます。ちなみにoperator*(*hp4のような書き方)はvalue()と違って中身が無いときの動作が未定義なので、確認済みの場面以外ではvalue()value_or()を使うのが安全です。

std::optionalのよくある失敗例と対処法

①確認せずにvalue()やoperator*を呼ぶ

上で実演した通りです。has_value()やif文での判定を挟むか、デフォルト値で済むならvalue_or()を使いましょう。

②「無い」と「エラー」を混同してしまう

std::optionalが表すのはあくまで「無いのが正常な結果でありうる」ケースです。ファイルが読めない・通信に失敗したなどの異常系には、optionalではなく例外やエラーコードを使う方が意図が伝わります。

③無駄に大きな型をoptionalで包んでコピーコストを増やす

optionalは中身の値をそのまま(コピーして)保持する仕組みなので、大きな構造体を何度もoptionalとしてやり取りするとコピーコストがかさみます。大きな型を扱うときは、参照やポインタと組み合わせる設計も検討しましょう。

注意点

  • 「値が無いかもしれない」はマジックナンバーではなくoptionalで表現する
  • 未確認のvalue()呼び出しは例外(bad_optional_access)になる
  • operator*は未確認だと未定義動作なので注意
  • 「無い」は正常系、異常系には例外やエラーコードを使う

まとめ

  • std::optionalは「値が無いかもしれない」を型で表現する仕組み
  • has_value() / value_or() / if文判定で安全に扱える
  • 未確認のvalue()実際に例外を投げることを確認した
  • 「無い」と「異常」は分けて考え、正常系にだけ使う

マジックナンバーで「無い」を表していた経験がある人ほど、std::optionalの安心感が実感しやすいと思います。実際にvalue()の例外まで確認しておくと、使うときの心構えもはっきりします。

ここまで読んでくださりありがとうございました。

関連記事