【C++】optional – 値が「ある」/「ない」を安全に表現する

C++

はじめに

C++でコードを書いていると、値を返せない場合というのがままあります。従来、このような場合は負の値やnullptrなど、通常出てこないであろう値を返すことで表現していました。しかし、これらの値が意味を持つ場合も考えられます。このような場合に、値がないことを安全に返すクラスがC++17で追加されました。それが本エントリで紹介するstd::optionalです。

本エントリでは、このstd::optionalを使い方を中心に紹介したいと思います。

定義

std::optional<T>は、次のどちらかの状態を持つクラステンプレートです。

  • Tの値を持っている。
  • 値を持っていない。

利用するには<optional>をインクルードします。

#include <optional>

std::optional<int> number = 42;           // 値あり
std::optional<int> empty;                 // 値なし
std::optional<int> missing = std::nullopt; // 値なしを明示

std::nulloptは「値なし」を表す定数です。これを使うことで、値の「ある」「なし」を区別できます。

使い方

値の有無を確認して取り出す

ifで値の有無を調べてから*で値を取り出します。

#include <iostream>
#include <optional>
#include <string>

int main()
{
    std::optional<int> count = 0;

    if(count) {
        std::cout << "count: " << *count << '\n';
    }

    std::optional<std::string> name = "Alice";

    if(name.has_value()) {
        std::cout << "name: " << *name << '\n';
        std::cout << "length: " << name->size() << '\n';
    }
}

if(count)で見ているのは、格納された数値が真かどうかではなく、値が存在するかどうかです。has_value()でも同じ判定ができます。クラスのメンバーには->でアクセスできます。

値の取り出し方

値の取り出し方は、値がなかった場合にどのように扱うかで変わってきます。

書き方用途値がない場合
*optopt->member存在を確認した値にアクセスするC++17では未定義動作
opt.value()値なしを例外として扱うstd::bad_optional_accessを送出
opt.value_or(default_value)代わりの値を使うdefault_valueを返す
#include <iostream>
#include <optional>

int main()
{
    std::optional<int> timeout;

    // 未設定なら30を使う。
    int seconds = timeout.value_or(30);
    std::cout << "timeout: " << seconds << '\n';

    try {
        std::cout << timeout.value() << '\n';
    } catch (const std::bad_optional_access&) {
        std::cout << "timeout is not set\n";
    }
}

value_or()は参照ではなく値を返します。元のoptionalは更新されません。

値を設定・削除する

値の設定には代入またはemplace()、値の削除にはreset()を使います。削除にはstd::nulloptを代入しても構いません。

#include <iostream>
#include <optional>
#include <string>

int main()
{
    std::optional<std::string> message;

    message = "Hello";
    std::cout << *message << '\n'; // Hello

    // std::string(5, '!')を直接構築する
    // すでに値がある場合は、その値を破棄してから構築する
    message.emplace(5, '!');
    std::cout << *message << '\n'; // !!!!!

    message.reset();
    std::cout << std::boolalpha << message.has_value() << '\n'; // false

    message = "Again";
    message = std::nullopt; // これも値なしに戻す操作
}

使用例

例1: 「見つからないかもしれない」検索処理

std::optionalがもっとも活きる典型例は、失敗する可能性のある検索処理です。

#include <optional>
#include <vector>
#include <string>
#include <iostream>

struct User {
    int id;
    std::string name;
};

std::optional<User> find_user(const std::vector<User>& users, int id)
{
    for(const auto& user : users) {
        if(user.id == id) {
            return user; // 見つかった場合はUserを返す
        }
    }

    return std::nullopt; // 見つからない場合
}

int main()
{
    std::vector<User> users = {{1, "Alice"}, {2, "Bob"}};

    if(auto result = find_user(users, 2)) {
        std::cout << "見つかりました: " << result->name << "\n";
    }
    else {
        std::cout << "ユーザーが見つかりません\n";
    }

    // 見つからない場合の例
    auto missing = find_user(users, 99);
    std::cout << "id=99のユーザー名: "
              << missing.value_or("不明") << "\n";
}

例2: 文字列から数値への安全な変換

#include <optional>
#include <string>
#include <charconv>
#include <iostream>

std::optional<int> parse_int(const std::string& str)
{
    int value{};
    auto result = std::from_chars(str.data(), str.data() + str.size(), value);

    if(result.ec == std::errc()) {
        return value;
    }

    return std::nullopt; // 変換失敗
}

int main()
{
    for(const auto& text : {"123", "abc", "45x"}) {
        if(auto num = parse_int(text)) {
            std::cout << text << " -> " << *num << "\n";
        }
        else {
            std::cout << text << " -> 変換失敗\n";
        }
    }
}

例3: 関数の任意引数(デフォルト値の代わり)

#include <optional>
#include <iostream>

void connect(const std::string& host, std::optional<int> port = std::nullopt)
{
    int actual_port = port.value_or(8080); // デフォルトポート
    std::cout << host << ":" << actual_port << " に接続します\n";
}

int main()
{
    connect("example.com");        // ポート省略 -> デフォルト値を使用
    connect("example.com", 443);   // ポート指定あり
}

注意点

std::optionalを使う際は、いくつか気をつけたい点があります。

参照は保持できない

std::optional<T&>は標準では許可されていません(C++26で追加予定です)。参照が必要な場合はstd::reference_wrapperを組み合わせるなどの工夫が必要です。

空のoptionalをそのまま参照しない

値がない状態で使うと未定義動作になるため、has_value()if(opt)での確認を習慣づけましょう。

「値がない」と「エラーの理由」は別物

std::optionalは「値があるかないか」しか表現できません。呼び出し側が失敗理由ごとに対応する必要があるなら、エラー情報を持つ独自の結果型などを検討してください。「存在しない」という情報だけで十分かどうかが、採用時の判断基準です。

まとめ

std::optional<T>は、「値がある」「値がない」を型で表現するための仕組みです。検索結果や省略可能な設定など、値の不在を自然に扱いたい場面で役立ちます。

  • 値なしを返すときはstd::nulloptを使う。
  • if(opt)またはhas_value()で確認し、*->でアクセスする。
  • 既定値を使うならvalue_or()、空を例外として扱うならvalue()を使う。
  • 値を消すときはreset()またはstd::nulloptの代入を使う。

まずは「見つからなければ特別な値を返す関数」を、std::optionalで表せないか考えてみてください。戻り値の意味が明確になり、呼び出し側でも値がない場合の処理を書きやすくなります。

C++

Posted by izadori