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)が確認しているのは、中に入っているboolがtrueかどうかではありません。
「値が存在しているかどうか」を確認しています。
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の使い方についてでした。
最後までお読みいただき、ありがとうございました。
