C++のoptionalの使い方について

AI実装検定のご案内

std::optionalは、「値が存在する場合」と「値が存在しない場合」の両方を表現できるC++の型です。

C++17で標準ライブラリに追加されました。

たとえば、ユーザーを検索する関数を考えてみましょう。

検索対象が見つかればUserオブジェクトを返せますが、見つからない可能性もあります。

このような場合にstd::optionalを使うと、

std::optional<User>

という型によって、「Userが存在するかもしれないし、存在しないかもしれない」という意味を明確に表現できます。

-1や空文字列などを特別な値として使う方法よりも、コードの意図が分かりやすくなるのが大きな特徴です。

目次

optionalを使用するための準備

optionalヘッダーをインクルードする

std::optionalを使用するには、<optional>ヘッダーをインクルードします。

#include <optional>

基本的な書き方は次のとおりです。

std::optional<型> 変数名;

整数を扱う場合は、次のようになります。

std::optional<int> number;

文字列の場合は次のとおりです。

#include <optional>
#include <string>

std::optional<std::string> name;

C++17以降を指定する

std::optionalはC++17から利用できます。

GCCやClangを使用している場合は、必要に応じて次のようにC++17以降を指定します。

g++ -std=c++17 main.cpp

C++23の機能であるand_then()transform()などを使用する場合は、C++23対応のコンパイラと設定が必要です。

optionalに値を格納する方法

初期化時に値を指定する

std::optionalには、通常の変数と同じように値を格納できます。

#include <optional>

std::optional<int> number = 100;

この場合、numberは整数値100を保持しています。

一方、次のように値を指定せずに作成すると、値を持っていない状態になります。

std::optional<int> number;

つまり、std::optional<int>には、

100などのint値が存在する状態
値が存在しない状態

の2種類があります。

後から値を代入する

作成後に値を代入することも可能です。

std::optional<int> number;

number = 100;

最初は値を持っていませんが、100を代入した時点で値を持つ状態になります。

optionalに値があるか確認する方法

std::optionalを使用するときは、値を取得する前に値が存在するか確認することが重要です。

has_value()を使用する

has_value()を使用すると、値が存在するかどうかを確認できます。

#include <iostream>
#include <optional>

int main()
{
    std::optional<int> number = 100;

    if (number.has_value()) {
        std::cout << "値があります\n";
    }
}

値が存在する場合はtrue、存在しない場合はfalseになります。

if文で直接判定する

std::optionalは真偽値として判定できるため、次のように簡潔に書くこともできます。

std::optional<int> number = 100;

if (number) {
    std::cout << "値があります\n";
}

実際のコードでは、この書き方もよく使用されます。

なお、この判定が確認しているのは「格納されている値の真偽」ではなく、「値が存在するかどうか」です。

optionalから値を取得する方法

std::optionalの値を取得する方法には、主にoperator*value()value_or()があります。

それぞれ動作が異なるため、違いを理解して使い分けることが重要です。

*演算子で値を取得する

値が存在していることが分かっている場合は、*を使って値を取得できます。

std::optional<int> number = 100;

std::cout << *number << '\n';

実行結果は次のとおりです。

100

ただし、空のstd::optionalに対して*を使用してはいけません。

C++17からC++23では、値を保持していないstd::optional*で参照すると未定義動作になります。

そのため、次のように値を確認してからアクセスするのが基本です。

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

value()で値を取得する

value()を使って値を取得する方法もあります。

std::optional<int> number = 100;

std::cout << number.value() << '\n';

値が存在すれば、その値が返されます。

一方、値が存在しない場合はstd::bad_optional_access例外が送出されます。

#include <iostream>
#include <optional>

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

    try {
        std::cout << number.value() << '\n';
    }
    catch (const std::bad_optional_access&) {
        std::cout << "値がありません\n";
    }
}

*とは異なり、空の場合に例外として処理できる点が特徴です。

value_or()で代替値を指定する

値が存在しない場合にデフォルト値を使用したいときは、value_or()が便利です。

std::optional<int> number;

std::cout << number.value_or(0) << '\n';

この場合、numberには値がないため、0が返されます。

一方、値が存在する場合はその値が使用されます。

std::optional<int> number = 100;

std::cout << number.value_or(0) << '\n';

実行結果は次のとおりです。

100

value_or()は、「値があればその値を使い、なければ指定した代替値を使う」という処理に適しています。

std::nulloptで値なしを表現する

nulloptの基本的な使い方

std::nulloptは、std::optionalが値を持っていない状態を明示的に表すための値です。

std::optional<int> number = std::nullopt;

この場合、number.has_value()falseになります。

すでに値を持っているoptionalを空にする場合にも使用できます。

std::optional<int> number = 100;

number = std::nullopt;

これによって、numberは値を持たない状態になります。

optionalを関数の戻り値として使う

検索結果を返す例

std::optionalの代表的な用途は、「値が得られないことが正常に起こり得る関数」の戻り値です。

たとえば、指定された整数を検索する関数を考えてみましょう。

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

std::optional<int> findNumber(
    const std::vector<int>& numbers,
    int target)
{
    for (int number : numbers) {
        if (number == target) {
            return number;
        }
    }

    return std::nullopt;
}

int main()
{
    std::vector<int> numbers = {10, 20, 30};

    auto result = findNumber(numbers, 20);

    if (result) {
        std::cout << "見つかりました: "
                  << *result << '\n';
    } else {
        std::cout << "見つかりませんでした\n";
    }
}

実行結果は次のとおりです。

見つかりました: 20

該当する値が見つからなければ、std::nulloptが返されます。

-1などの特殊値を使う方法との違い

従来は、検索に失敗した場合に-1などを返す方法もよく使われていました。

int findNumber()
{
    return -1;
}

しかし、この方法では-1が本来のデータなのか、「見つからなかった」という意味なのかを関数の仕様から判断する必要があります。

std::optionalを使えば、

std::optional<int> findNumber();

という戻り値の型そのものが、「値が存在しない可能性がある」ことを示します。

そのため、コードの意図を明確にしやすくなります。

文字列を整数に変換する例

変換に成功した場合だけ値を返す

文字列を整数に変換し、変換できない場合にstd::nulloptを返すこともできます。

#include <optional>
#include <stdexcept>
#include <string>

std::optional<int> toInt(const std::string& str)
{
    try {
        std::size_t pos = 0;
        int value = std::stoi(str, &pos);

        if (pos != str.size()) {
            return std::nullopt;
        }

        return value;
    }
    catch (const std::invalid_argument&) {
        return std::nullopt;
    }
    catch (const std::out_of_range&) {
        return std::nullopt;
    }
}

"123"であれば123が返されます。

一方、"abc"のように整数として変換できない文字列ではstd::nulloptが返されます。

std::stoi("123abc")は先頭の123だけを変換できるため、文字列全体を整数として扱いたい場合は、上の例のように変換位置も確認する必要があります。

構造体やクラスをoptionalに格納する方法

ユーザー定義型も格納できる

std::optionalには、整数や文字列だけでなく構造体やクラスも格納できます。

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

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

std::optional<User> findUser(bool found)
{
    if (found) {
        return User{"Taro", 30};
    }

    return std::nullopt;
}

呼び出し側では次のように使用できます。

auto user = findUser(true);

if (user) {
    std::cout << user->name << '\n';
    std::cout << user->age << '\n';
}

operator->でメンバーにアクセスする

クラスや構造体のメンバーを参照する

std::optionalが構造体やクラスを保持している場合、->を使ってメンバーへアクセスできます。

std::optional<User> user = User{"Taro", 30};

if (user) {
    std::cout << user->name << '\n';
}

次の書き方とほぼ同じ意味です。

std::cout << (*user).name << '\n';

ただし、operator*と同様に、値が存在していることを確認してから使用する必要があります。

reset()でoptionalを空にする

保持している値を破棄する

reset()を使用すると、optionalが保持している値を破棄できます。

std::optional<int> number = 100;

number.reset();

この後、numberは値を持たない状態になります。

if (!number) {
    std::cout << "値がありません\n";
}

次の書き方でも同様に値をなくせます。

number = std::nullopt;

emplace()で値を直接構築する

optional内部にオブジェクトを生成する

emplace()を使用すると、optionalの内部に値を直接構築できます。

#include <optional>
#include <string>
#include <utility>

struct User {
    std::string name;
    int age;

    User(std::string n, int a)
        : name(std::move(n)), age(a)
    {
    }
};

次のように使用できます。

std::optional<User> user;

user.emplace("Taro", 30);

optionalがすでに値を持っている場合は、既存の値を破棄してから新しい値が構築されます。

構造体やクラスを格納するときに便利な方法です。

make_optional()でoptionalを作成する

型推論を利用して作成する

std::make_optional()を使用してoptionalを作ることもできます。

auto number = std::make_optional(100);

この場合、numberの型は次のようになります。

std::optional<int>

文字列の場合は次のように書けます。

auto text = std::make_optional<std::string>("Hello");

optionalを関数の引数として使う

未指定の状態を受け取りたい場合に便利

std::optionalは関数の戻り値だけでなく、引数にも使用できます。

たとえば、年齢を指定してもしなくてもよい関数なら次のように書けます。

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

void printUser(
    const std::string& name,
    std::optional<int> age)
{
    std::cout << "名前: " << name << '\n';

    if (age) {
        std::cout << "年齢: " << *age << '\n';
    } else {
        std::cout << "年齢: 未設定\n";
    }
}

呼び出し側では次のように使用できます。

printUser("Taro", 30);

または、

printUser("Taro", std::nullopt);

と書けます。

optionalを引数に使うのは、「値を指定しない状態そのものに意味がある場合」に特に適しています。

optionalとポインタの違い

optionalは基本的に値そのものを保持する

std::optionalには*->があるため、ポインタと似て見えることがあります。

しかし、両者の役割は異なります。

std::optional<User> user;

では、値が存在する場合、Userオブジェクトはoptional内部に保持されます。

一方、

User* user;

は、基本的に別の場所に存在するUserオブジェクトを指すためのポインタです。

したがって、

std::optional<T>

は、

Tが存在する
Tが存在しない

という状態を表す目的に適しています。

ポインタは、オブジェクトへの参照や動的メモリ管理、所有関係など別の目的でも使用されます。

単純に「値の有無」を表現したいだけであれば、std::optionalの方が設計意図を明確にしやすいケースがあります。

optionalを使うときの注意点

falseでも値は存在している

std::optional<bool>は少し注意が必要です。

std::optional<bool> flag = false;

この場合、flagにはfalseという値が存在しています。

そのため、

if (flag) {
    std::cout << "実行されます\n";
}

if文は成立します。

if (flag)が確認しているのは、中に入っているbooltrueかどうかではありません。

「値が存在しているかどうか」を確認しています。

std::optional<bool>には、

true
false
値なし

という3状態が存在します。

中身のboolを確認したい場合は、値の存在を確認したうえで*flagなどを使います。

if (flag && *flag) {
    std::cout << "値はtrueです\n";
}

C++23のand_then()の使い方

optionalを返す処理を連結できる

C++23では、std::optionalにモナディック操作が追加されました。

その一つがand_then()です。

and_then()は値が存在するときだけ関数を実行します。

呼び出す関数はstd::optionalを返します。

#include <optional>

std::optional<int> doubleValue(int value)
{
    if (value < 0) {
        return std::nullopt;
    }

    return value * 2;
}

次のように使用できます。

std::optional<int> number = 10;

auto result = number.and_then(doubleValue);

numberに値が存在すればdoubleValue()が実行されます。

値がなければ、関数は呼び出されず、空のoptionalが返されます。

C++23のtransform()の使い方

optionalの値を別の型に変換できる

transform()は、値が存在する場合だけ変換処理を実行します。

#include <optional>
#include <string>

std::optional<int> number = 100;

auto text = number.transform([](int value) {
    return std::to_string(value);
});

この場合、textの型は次のようになります。

std::optional<std::string>

numberが空なら、変換用の関数は実行されず、textも空になります。

C++23のor_else()の使い方

値がない場合だけ代替処理を実行する

or_else()は、optionalに値が存在しない場合だけ処理を実行します。

#include <optional>

std::optional<int> number;

auto result = number.or_else([] {
    return std::optional<int>{100};
});

この例ではnumberが空なので、resultには100が入ります。

一方、元のoptionalに値がある場合は、その値がそのまま使用されます。

C++17とC++23のoptionalの違い

主な機能を整理する

std::optionalの主な機能と対応規格は次のとおりです。

機能用途対応規格
std::optional値の有無を表すC++17
std::nullopt値なしを表すC++17
has_value()値が存在するか確認するC++17
value()値を取得するC++17
value_or()値がなければ代替値を返すC++17
reset()保持している値を破棄するC++17
emplace()内部に値を構築するC++17
std::make_optional()optionalを生成するC++17
and_then()optionalを返す処理を連結するC++23
transform()値を変換するC++23
or_else()値がない場合の代替処理を行うC++23

基本的なstd::optionalの利用であれば、C++17の機能だけでも十分に活用できます。

C++26でのoptionalの変更点

rangeとして扱えるようになる

C++26では、std::optionalを0個または1個の要素を持つviewとして扱えるようになります。

値が存在する場合は1要素のrange、値が存在しない場合は0要素のrangeとして扱えるイメージです。

ただし、一般的なC++17・C++20・C++23向けのコードを書く場合は、必ずしも意識する必要はありません。

optionalを使うときの注意点

空のoptionalを*や->で参照しない

最も重要なのは、値を持っていないoptionalに対して*->を使用しないことです。

std::optional<int> number;

std::cout << *number;

C++17からC++23では、このようなアクセスは未定義動作になります。

基本的には次のように確認してから値を取得します。

if (number) {
    std::cout << *number;
}

また、値がない場合を例外として扱いたい場合はvalue()を使用できます。

何でもoptionalにしない

std::optionalは便利ですが、すべての値をoptionalにする必要はありません。

std::optional<int> age;

とする場合は、「年齢が存在しない状態」がプログラム上必要かどうかを考える必要があります。

必ず値が存在する設計であれば、通常の型を使う方がシンプルです。

int age;

std::optionalは、「値がないことも正常な状態として扱いたい場合」に使用するのが基本です。

エラーの理由までは表現できない

std::optionalが表現できるのは基本的に、

値がある
値がない

という2つの状態です。

たとえば、

ファイルが存在しない
アクセス権限がない
データ形式が不正

といった複数の失敗理由を区別することはできません。

そのような場合は、C++23のstd::expected<T, E>などの利用を検討できます。

std::optionalは「成功か失敗か」を詳細に表現する型というよりも、「値が存在するかどうか」を表現する型と考えると分かりやすいでしょう。

optionalが向いているケース

値が存在しないことが正常に起こる場合

std::optionalは、次のようなケースに向いています。

  • 検索対象が見つからない可能性がある場合
  • 設定値が未指定になる可能性がある場合
  • 文字列などの変換結果が得られない場合
  • データベースなどの項目がNULLになり得る場合
  • -1や空文字列を特殊な値として使いたくない場合
  • 「未設定」と「設定済み」を明確に区別したい場合
  • 関数の引数で「指定しない」という状態を表現したい場合

たとえば、

std::optional<User> findUser(int id);

という宣言を見るだけで、「Userが見つからない可能性がある関数」であることが分かります。

型によってAPIの意味を明確にできることが、std::optionalの大きなメリットです。

C++のoptionalの使い方まとめ

std::optionalは、「値が存在する場合」と「値が存在しない場合」を安全かつ明確に表現するための型です。

基本的な使い方は次のようになります。

#include <iostream>
#include <optional>

std::optional<int> getNumber(bool success)
{
    if (success) {
        return 100;
    }

    return std::nullopt;
}

int main()
{
    auto result = getNumber(true);

    if (result) {
        std::cout << "値: " << *result << '\n';
    } else {
        std::cout << "値がありません\n";
    }
}

まず、

std::optional<T>

によって「T型の値が存在するかもしれない状態」を表します。

値が存在しないことを明示するときは、

std::nullopt

を使用します。

値の有無は、

if (result)

または、

result.has_value()

で確認できます。

値を取得する場合は、用途に応じて、

*result
result.value()
result.value_or(defaultValue)

を使い分けます。

特に注意したいのは、空のoptionalに対して*->でアクセスしないことです。

C++17からC++23では未定義動作になるため、必ず値の存在を確認してから使用する必要があります。

std::optionalを適切に使用すると、-1や空文字列などの特殊な値に依存するコードを減らせます。

また、関数や変数が「値なし」という状態を取り得ることを型そのもので表現できるため、コードの意図が明確になり、保守性や可読性の向上にもつながります。

以上、C++のoptionalの使い方についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次