LoginSignup
3
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-25

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

C++ Advent Calender 2022

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

<<16日目 | 本当のコンパイル時variant || 22日目 | C++にモナドはいらない >>

はじめに

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

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

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

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

作り込んだバグ

#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>型の変数datestd::chrono::sys_days型の定数thresholdを比較するoperator<=を呼び出しています。

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

問題点

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

20221124
20221201
20221A05

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

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

  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を返してしまうのです。

修正方法

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の呼び出し)。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2