1. ホーム
  2. C++

C++17のstd::optionalについて

2022-02-24 07:46:38

    プログラミングをしていると、ある型のオブジェクトを返す/渡す/使うという場面によく遭遇します。つまり、ある型の値を持つこともあれば、値を持たないこともある。そこで、ポインターのようなセマンティクスをモデル化する方法が必要です。そこでは、値がないことを示すためにnullptrを使用することができます。
これを扱う方法は、特定の型のオブジェクトを定義し、値が存在するかどうかを示すために追加のブール型メンバ/フラグを使用することです。std::optional<>は、型安全な方法でそのようなオブジェクトを提供します。

    std::optional オブジェクトは、単純に含むオブジェクトの内部メモリにブーリアン・フラグを加えたものです。その結果、サイズは通常、包含オブジェクトより1バイト大きくなります。いくつかの包含型では、包含オブジェクトに追加情報を置くことができるならば、サイズのオーバーヘッドが全くないことさえあります。ヒープメモリは割り当てられません。オブジェクトは、含まれる型と同じアライメントを使用します。

    しかし、std::optionalオブジェクトは、単に値のメンバにブーリアン・フラグ関数を追加するだけの構造物ではありません。例えば、値がない場合、含まれる型のコンストラクタは呼ばれません(したがって、オブジェクトは値なしというデフォルトの状態を提供することができます)。

    std:: variable <> や std::any と同様に、生成されるオブジェクトはすべて値のセマンティクスを持っています。つまり、コピーは、それ自身のメモリに値が含まれている場合、その値を含むトークンを持つ別のオブジェクトを作成するディープコピーとして実装されています。値を含まないstd::optional<>をコピーするのは安直である。
std::optional<> に含まれる値をコピーすることは、含まれる型/値をコピーすることと同様に安価/高コストです。移動セマンティクスがサポートされています。

1.1 std::optional<>

std::optional<> は、任意の型の null 可能なインスタンスをモデル化します。インスタンスにはメンバー、パラメータ、戻り値などがあります。また、std::optional<>は0または1の要素を含むコンテナであると考えてください。

1.1.1 std::optional<> 返り値

次のプログラムは、std::optional<>の戻り値としての取り扱いを示しています。

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

// convert string to int if possible:
std::optional<int> asInt(const std::string& s)
{
    try 
    {
        return std::stoi(s);
    }
    catch (...) 
    {
        return std::nullopt;
    }
}
int main()
{
    for (auto s : { "42", " 077", "hello", "0x33" }) 
    {
        // convert s to int and use the result if possible:
        std::optional<int> oi = asInt(s);
        if (oi) {
            std::cout << "convert '" << s << "' to int: " << *oi << "\n";
        }
        else {
            std::cout << "can't convert '" < < < s < < "' to int\n";
        }
    }
}

プログラム中では、asInt()は、渡された文字列を整数に変換する関数です。しかし、これではうまくいかないかもしれない。そこで、std::optional<>を使って、"no int" を返し、そのために特別なint値を定義したり、呼び出し元に対して例外を投げたりすることを避けることができるのです。つまり、返り値を int で初期化する stoi() の呼び出し結果を返すか、int 値がないことを示す std::nullopt を返すかのどちらかです。

結果は次のようになります。

以下のようにすれば、同じ動作を実現できます。

std::optional<int> asInt(const std::string& s)
{
    std::optional<int> ret; // initially no value
    try 
    {
        ret = std::stoi(s);
    }
    catch (...) 
    {
    }
    return ret;
}

main()では、この関数を別の文字列で呼び出しています。

for (auto s : {"42", " 077", "hello", "0x33"} ) 
{
    // convert s to int and use the result if possible:
    std::optional<int> oi = asInt(s);
    ...
}

返された各 std::optional<int> oi に対して、(オブジェクトをブール式として計算することによって)値があるかどうかを計算し、オプションオブジェクトを "dereferencing" することによってその値にアクセスするのです。

if (oi) 
{
    std::cout << "convert '" << s << "' to int: " << *oi << "\n";
}

stoi() は文字列を16進数にパースしないので、文字列 "0x33" に対して asInt() を呼び出すと 0 が生成されることに注意しましょう。

また、この戻り値は、.NET Framework のようなメソッドで処理することができます。

std::optional<int> oi = asInt(s);
if (oi.has_value()) 
{
    std::cout << "convert '" << s << "' to int: " << oi.value() << "\n";
}

    ここでhas_value()は戻り値があるかどうかをチェックし、もしあればvalue()でそれを取得するために使用します。value()は演算子*よりも安全です。なぜなら値なしでインターフェースを呼び出すと例外が発生するからです。演算子*は値があることが確実な場合のみ使用すべきであり、そうでない場合はプログラムが未定義の動作をすることになります。

    なお、asInt()は、新しい型であるstd::string_viewを使用することで改善されることがあります。

1.1.2 std::optional<> パラメータとデータメンバ

std::optional<> を使用するもう一つの例として、std::optional によるパラメータとデータメンバとしての受け渡しがあります。

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

class Name
{
private:
    std::string first;
    std::optional<std::string> middle;
    std::string last;
public:
    Name(std::string f,
        std::optional<std::string> m,
        std::string l)
        : first{ std::move(f) }, middle{ std::move(m) }, last{ std::move(l) } 
    {
    }
    friend std::ostream& operator << (std::ostream& strm, const Name& n) 
    {
        strm << n.first << ';
        if (n.middle) {
            strm << n.middle.value() << ' ';
        }
        return strm << n.last;
    }
};
int main()
{
    Name n{ "Jim", std::nullopt, "Knopf" }
    std::cout << n << '\n';
    Name m{ "Donald", "Ervin", "Knuth" };
    std::cout << m << '\n';

    return 0;
}


    Nameというクラスは、ファーストネーム、オプションのミドルネーム、ラストネームから構成されることを表しています。メンバ middle は std::optional<> として定義され、コンストラクタでは middle name が空文字列である状態とは異なり、middle name がない場合は std::nullopt 引数を渡すことができるようになっています。

    なお、値のセマンティクスを持つ型では、対応するメンバを初期化するコンストラクタを定義するには、引数を値で取得してメンバに移動するのが最適な方法です。

   また、std::optional<>は、メンバの中間値へのアクセスを変更することにも注意してください。middleをboolean式として使用することで、middleがあるかどうかを判断し、もしあればvalue()メソッドで値を取得することができます。

    オプションの値にアクセスするもう一つの方法は、メンバ関数 value_or() で、値が存在しない場合に指定した値を許可するものである。例えば、クラス名の内部では、. を実装することもできます。

std::cout << middle.value_or(""); // print middle name or nothing

1.2 std::optional<> 型と操作

ここでは、std::optional<>の型と操作について詳しく説明します。

1.2.1 std::optional<> 型

    std::optional<> は,標準ライブラリヘッダーファイル <optional> で次のように定義されています.

namespace std 
{
template<typename T> class optional;
}

また、型やオブジェクトは以下のように定義されています。

  • std::nullopt_t 型の nullopt は、値を持たないオブジェクトである。
  • std:: Exception から派生した例外クラス std::bad_optional_access は、値にアクセスできない std::optional<> オブジェクトに対してスローされる例外です。

また、オプショナルオブジェクトは、<utility> で定義されている std::in_place (std::in_place_t 型) を使用して、複数の引数を持つオプショナルオブジェクトの値を初期化します (後述)。

1.2.2 std::optional<>操作

以下のstd::optionalの操作は、std::optional<>で提供されるすべての操作の一覧を示します。

<テーブル <キャプション std::optionalのメンバは、以下のように設定されます。          メンバー定義                                           説明 コンストラクタ オプションのオブジェクトを作成する(型を含むコンストラクタを呼び出す可能性がある)。 make_optional<>() オプショナルオブジェクトを作成する(値を渡して初期化する) デストラクタ オプションのオブジェクトを破棄する 新しい値を割り当てる emplace() 含まれる型に新しい値を割り当てる リセット() 任意の値を破棄する(オブジェクトを空にする) has_value() オブジェクトが値を持っているかどうかを返します ブール値への変換 オブジェクトが値を持つかどうかを返します アクセス値 (値がない場合は未定義の動作) -> 値にアクセスするためのメンバー (値がない場合は未定義の動作) 値() 値にアクセスする(値がない場合は例外を投げる) 値_or() 値にアクセスする(値がない場合は供給された値を使用する)。 swap() 2つのオブジェクトの値を入れ替える は、==、<、<=、>、>=を表します。 2つのオプションオブジェクトを比較する ハッシュ<> ハッシュ値を計算するための関数オブジェクトタイプ
  1. コンストラクター

    特殊なコンストラクタにより、含まれる型に直接パラメータを渡すことができます。

  • 値を持たないオプショナルオブジェクトを作成することも可能です。この場合、含む型を指定する必要があります。
std::optional<int> o1;
std::optional<int> o2(std::nullopt);

ここでは、含まれる型に対してコンストラクタは呼び出されません。

  • インクルードされた型を初期化するための値を渡すことができます。派生ガイドによると、以下のように、インクルードタイプを指定する必要はない。
std::optional o3{42}; // deduces optional<int>
std::optional<std::string> o4{"hello"};
std::optional o5{"hello"}; // deduces optional<const char*>

複数の引数を持つオプションのオブジェクトを初期化するには、オブジェクトを作成するか、std::in_placeを最初の引数として追加しなければなりません(含まれる型は推論できません)。

std::optional o6{std::complex{3.0, 4.0}};
std::optional<std::complex<double>> o7{std::in_place, 3.0, 4.0};

2番目の形式は、一時的なオブジェクトの作成を避けることができることに注意してください。この形式を使うことで、初期化子のリストや追加パラメータを渡すことも可能です。

// initialize set with lambda as sorting criterion:
auto sc = [] (int x, int y) 
{
    return std::abs(x) < std::abs(y);
};

std::optional< std::set< int,decltype(sc)>> o8{std::in_place, {4, 8, -7, -2, 0, 5}, sc};

  • オプションのオブジェクトをコピーできる(型変換を含む)。
std::optional o5{"hello"}; // deduces optional<const char*>
std::optional<std::string> o9{o5}; // OK

便利な関数 make_optional<>() もあり、単一または複数の引数で初期化できることに注意してください (in_place 引数は必要ありません)。通常通り、make......関数は......を導出します。

auto o10 = std::make_optional(3.0); // optional<double>
auto o11 = std::make_optional("hello"); // optional<const char*>
auto o12 = std::make_optional<std::complex<double>>(3.0, 4.0);

ただし、値を受け取って、その値でオプションの値を初期化するか、その値に基づいてnulloptを使用するかを決定するコンストラクタは存在しないことに注意してください。例えば、演算子 ?

std::multimap<std::string, std::string> englishToGerman;
...
auto pos = englishToGerman.find("wisdom");
auto o13 = pos ! = englishToGerman.end()
? std::optional{pos->second}
: std::nullopt;

    ここでは、クラステンプレート・パラメータ std::optionalf(pos->second) の導出により、o13 は std::optional<std::string> に初期化されています。std::nulloptクラステンプレート引数の導出はうまくいきませんが、演算子 ? : は導出の際に式の結果型をこの型に変換します。

2. 値へのアクセス

オプションのオブジェクトが値を持つかどうかを調べるには、 オブジェクトをブール式に使用するか、あるいは has_value() をコールします。

std::optional o{42};
if (o) ... // true
if (!o) ... // false
if (o.has_value()) ... // true

  値にアクセスするために、ポインタの構文が用意されています。演算子 * は、それが含む型のオブジェクトの値に直接アクセスすることを可能にし、演算子 -> は、それが含む型のオブジェクトのメンバにアクセスすることを可能にします。

std::optional o{std::pair{42, "hello"}};
auto p = *o; // initializes p as pair<int,string>
std::cout << o->first; // prints 42

これらの演算子は、オプションに値が含まれていることが必要であることに注意してください。値なしでこれらを使用することは、未定義の動作です。

std::optional <std::string> o{"hello"};
std::cout << *o; // OK: prints "hello"
o = std::nullopt;
std::cout << *o; // undefined behavior

2番目の出力は、オプションのオブジェクト値に使用される基礎となるメモリが変更されていないため、実際にはまだコンパイルされ、再び "hello"を印刷するなど、いくつかの出力が実行されることに注意してください。
しかし、これに頼ることはできないし、頼るべきではありません。オプショナルオブジェクトが値を持っているかどうかわからない場合は、以下の関数を呼び出すしかないでしょう。

if (o) std::cout << *o; // OK (might output nothing)

また、value() を使うこともできます。これは、値が含まれていない場合、std::bad_optional_access 例外を投げます。

std::cout << o.value(); // OK (throws if no value)

bad_optional_access は std::exception から直接派生しています。

最後に、オプショナルオブジェクトに値がない場合、その値を要求し、フォールバック値を渡せば、その値が使われます。

std::cout << o.value_or("fallback"); // OK (outputs fallback if no value)

fallback引数はrvalue参照として渡されるため、fallbackが使用されない場合はコストがかからず、fallbackが使用される場合は移動セマンティクスをサポートします。

3 比較演算

    通常の比較演算子が使用できる。オペランドはオプショナルオブジェクト、インクルードタイプのオブジェクト、std::nulloptが使用可能です。

  • オペランドの両方が値を持つオブジェクトである場合、含まれる型の対応する演算子が使用されます。
  • オペランドの両方が値を持たないオブジェクトである場合、それらは等しいとみなされます(==は真を、他のすべての比較は偽を生成します)。
  • オペランドの一方だけが値を持つオブジェクトである場合、値を持たないオペランドはもう一方のオペランドより小さいとみなされます。

std::optional<int> o0;
std::optional<int> o1{42};
o0 == std::nullopt // yields true
o0 == 42 // yields false
o0 < 42 // yields true
o0 > 42 // yields false
o1 == 42 // yields true
o0 < o1 // yields true

これは、オプショナルオブジェクトが符号なし整数の場合は0より小さい値、boolオプショナルの場合は0より小さい値を持つことを意味します。

std::optional<unsigned> uo;
uo < 0 // yields true
std::optional<bool> bo;
bo < false // yields true

ここでも、基礎となる型の暗黙の型変換がサポートされています。

std::optional<int> o1{42};
std::optional<double> o2{42.0};

o2 == 42 // yields true
o1 == o2 // yields true

オプションのbool値や生のポインタ値は、驚きをもたらすかもしれないことに注意してください。

4 値の修正

割り当てと配置による値の変更。

std::optional<std::complex<double>> o; // has no value
std::optional ox{77}; // optional<int> with value 77
o = 42; // value becomes complex(42.0, 0.0)
o = {9.9, 4.4}; // value becomes complex(9.9, 4.4)
o = ox; // OK, because int converts to complex<double>
o = std::nullopt; // o no longer has a value
o.emplace(5.5, 7.7); // value becomes complex(5.5, 7.7)

std::nulloptを代入すると値が削除され、以前から値があった場合は、含まれる型のデストラクタを呼び出すことになります。また、reset()を呼び出しても同じ効果を得ることができます。

o.reset(); // o no longer has a value

または空白の括弧への代入。

o = {}; // o no longer has a value

最後に、演算子*は参照によって値を生成するので、値を修正するために使うこともできます。ただし、この場合、存在する値を変更する必要があることに注意してください。

std::optional<std::complex<double>> o;
*o = 42; // undefined behavior
...
if (o) 
{
    *o = 88; // OK: value becomes complex(88.0, 0.0)
    *o = {1.2, 3.4}; // OK: value becomes complex(1.2, 3.4)
}

5 モバイルセマンティクス

    std::optional<> は、移動セマンティクスもサポートしています。オブジェクトが全体として移動される場合、状態はコピーされ、含まれているオブジェクトも(もしあれば)移動されます。したがって、あるオブジェクトから移動されたオブジェクトは、まだ同じ状態を持ちますが、移動された値は不特定多数になります。ただし、含まれるオブジェクトとの間で値を移動することも可能である。例えば

std::optional<std::string> os;
std::string s = "a very very very long string";
os = std::move(s); // OK, moves
std::string s2 = *os; // OK copies
std::string s3 = std::move(*os); // OK, moves

    os は最後の呼び出しの後でも文字列の値を持っていますが、その値は os オブジェクトの指定がないことに注意してください。したがって、移動したオブジェクトの値について仮定して使用することはできません。もちろん、移動したオブジェクトに新しい値を代入することは可能です。

ビジュアルスタジオでデバッグすると、os.have_valueは文字列が空であることを除いて、tureを返します。

6 ハッシュ化

std::optional オブジェクトのハッシュは、含まれる非定数型(もしあれば)のハッシュである。

1.3 特殊なケース

    特定のオプション値のタイプは、特別または予期しない動作をもたらす可能性があります。

1.3.1 bool または raw ポインタのオプション

比較演算子を使用することは、オプショナルオブジェクトをブール値として使用することとは異なるセマンティクスを持つことに注意してください。これは、含まれる型がboolやポインタ型である場合、混乱することがあります:例えば。

std::optional<bool> ob{false}; // has value, which is false
if (!ob) ... // yields false --> determine if ob contains a bool value
if (ob == false) ... // yields true -->determine if ob contains a bool value which is false
std::optional<int*> op{nullptr};
if (!op) ... // yields false --> determine if ob contains a pointer value
if (op == nullptr) ... // yields true --> determine if ob contains a pointer value equal to nullptr


1.3.2 オプショナルのオプショナル

原則的に、オプショナルにはオプショナル値を定義することもできます。

std::optional<std::optional<std::string>> oos1;
std::optional<std::optional<std::string>> oos2 = "hello";
std::optional<std::optional<std::string>> oos3{std::in_place, std::in_place, "hello"};
std::optional<std::optional<std::complex<double>>> ooc{std::in_place, std::in_place, 4.2, 5.3};

また、暗黙の変換でも、新しい値を割り当てることができます。

oos1 = "hello"; // OK: assign new value
ooc.emplace(std::in_place, 7.2, 8.3);



オプショナルには2つのレベルがあるので、オプショナルのオプショナルは "値なし" を外部または内部に出現させ、異なる意味づけをすることができます。

*oos1 = std::nullopt; // inner optional has no value
oos1 = std::nullopt; // outer optional has no value

ただし、オプショナルな値の扱いには特に注意が必要です。

if (!oos1) std::cout << "no value\n";
if (oos1 && ! *oos1) std::cout << "no inner value\n";
if (oos1 && *oos1) std::cout << "value: " << **oos1 << '\n';