std::optionalとの比較演算子に潜む見落としがちな罠

# std::optionalとの比較演算子に潜む見落としがちな罠

## C++ Advent Calender 2022

この記事はC++ Advent Calender 2022 21日目の記事です。・・・もうクリスマス終わってしまったorz

[\<\<16日目 | 本当のコンパイル時variant](https://zenn.dev/wx257osn2/articles/constexpr_variant-jdbnlwiberawejbf) || [22日目 | C++にモナドはいらない >>](https://qiita.com/soreja/items/4363188057a0f2697022)

## はじめに

C++17で`std::optional`が追加されてからもう5年も立ちました。皆さん使っていますか？

https://cpprefjp.github.io/reference/optional/optional.html

```cpp
std::optional<int> hoge()
{
    int ret = /*somehing*/;
    if (/*なんかの条件*/) return {};
    return ret;
}
int main()
{
    const auto ret = hoge();
    if (!ret) return 1;
    std::cout << *ret << std::endl;
}
```

雑に言えばこんな感じに無効値を持てるクラスでした。

上の例で言えば、`return {}`する場合、`std::optional`クラスのデフォルトコンストラクタが呼び出されるために無効値として初期化された`std::optional`クラスが関数の戻り値となり、`return ret;`する場合は変数`ret`に入っていた値を持つ`std::optional`クラスが関数の戻り値となるのでした。

## 比較演算子

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/8e58b750-1e96-2d82-6639-466cf144e027.png)

`std::optional`クラスは比較演算子を持ちます。

https://cpprefjp.github.io/reference/optional/optional/op_less_equal.html

ということは自分で`operator*`で中の値を取ってから比較しなくても良さそう、そんなふうに思いますよね？

## 作り込んだバグ

```cpp
#include <optional>
#include <chrono>
#include <string>
#include <ranges>
#include <vector>
#include <regex>
#include <iostream>
namespace ch = std::chrono;
namespace views = std::ranges::views;
std::optional<ch::sys_days> to_date(const std::string& s) noexcept
{
  try {
    static const std::regex reg(R"A((\d{4})(\d{2})(\d{2}))A");
    std::smatch matched;
    if (!std::regex_search(s, matched, reg) || matched.size() != 4) return std::nullopt;
    const ch::year_month_day ymd(ch::year{ std::stoi(matched[1].str()) }, ch::month(std::stoi(matched[2].str())), ch::day(std::stoi(matched[3].str())));
    return ymd;
  }
  catch (const std::exception&) {
    return std::nullopt;
  }
}
const auto filter_by_date = [](const std::string& s) noexcept -> bool
{
  using namespace std::chrono_literals;
  constexpr ch::sys_days threshold = 2022y/12/01;
  const auto date = to_date(s);
  return date <= threshold;
};
int main()
{
  std::vector<std::string> inputs = { "20221124", "20221201", "20221202", "20221205", "20221A05" };
  for (const auto& input : inputs | views::filter(filter_by_date)) {
    std::cout << input << std::endl;
  }
}
```

`to_date`は文字列を`std::chrono::sys_days`に変換することを試みて、そのoptionalを返す関数です。

`filter_by_date`の中で`std::optional<std::chrono::sys_days>`型の変数`date`と`std::chrono::sys_days`型の定数`threshold`を比較する`operator<=`を呼び出しています。

ではどこが問題なのでしょうか？

## 問題点

https://wandbox.org/permlink/RVSTVeTEqlp2KIas

上記のプログラムを実行すると出力は次のようになります

```
20221124
20221201
20221A05
```

`20221A05`は`(\d{4})(\d{2})(\d{2})`という正規表現にマッチしないはずの入力です。にも関わらずなぜ出力されてきているのでしょうか。

ここで改めてcpprefjpを見てみます。

https://cpprefjp.github.io/reference/optional/optional/op_less_equal.html


```cpp
  template <class T, class U>
  constexpr bool operator<=(const optional<T>& x, const U& y);           // (2) C++17
```

`return x.has_value() ? x.value() <= v : true;`を返すとあります。

`20221A05`が入力として与えられたとき、`to_date`での変換は失敗するので`std::nullopt`を返します。

ところが、値を持たないoptionalとその内部型との比較演算子`operator<=`は、`true`を返してしまうのです。

## 修正方法

```cpp
const auto filter_by_date = [](const std::string& s) noexcept -> bool
{
  using namespace std::chrono_literals;
  constexpr ch::sys_days threshold = 2022y/12/01;
  const auto date = to_date(s);
  // std::nulloptとの比較はtrueになるため弾く
  return date && date <= threshold;
};
```

予めoptionalが値を持つか検査すればいいのでこのようになります(`explicit operator bool`の呼び出し)。

https://wandbox.org/permlink/QRzDtgBvGr5xiKQ3