「検索して見つからなかったとき、-1やnullptrを返すのってちょっと気持ち悪い」「std::optionalって結局何が嬉しいの?」「中身が無いときにアクセスするとどうなるの?」
こんな疑問はありませんか?
今回は、C++17で追加されたstd::optionalについて、実際にコンパイル・実行した結果を交えながら解説していきます。「値が無いかもしれない」をどう表現するのが安全なのか、実例で見ていきます。
std::optionalとは?
std::optionalとは、
・「値があるかもしれないし、無いかもしれない」という状態そのものを型で表現できるテンプレートクラス
です。これまで「値が無い」ことを表すのに、ポインタならnullptr、整数なら-1や0のようなマジックナンバーを使うことがよくありました。しかし-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を確認せず参照外しするとクラッシュ(未定義動作)しますが、optionalのvalue()はちゃんと例外という形でエラーを教えてくれます。ちなみに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()の例外まで確認しておくと、使うときの心構えもはっきりします。
ここまで読んでくださりありがとうございました。


