【C++】一貫比較 – 三方比較演算子(<=>)

2024-04-16C++

C++20にて一貫比較と呼ばれる機能が追加されました。これを行う演算子が三方比較演算子(<=>)です。これを使うと比較演算子の実装が簡単になります。本エントリでは、この三方比較演算子について調べてみたのでまとめてみたいと思います。

三方比較演算子とは

C++20で追加された新しい二項演算子です。その見た目から宇宙船演算子とも呼ばれます。この演算子を使うと、最大6種類の比較演算子(<, <=, >, >=, ==, !=)を実装するのが容易となります。

動作

ある値a, bに対する三方比較演算子の結果は、「より大きい」「より小さい」「等しい」の3つの関係を同時に表す値を返します。これは比較カテゴリ型と呼ばれる型であり、単純なboolではありません。

bool値にするためには、戻り値と0を比較する必要があります。なお、比較カテゴリ型のクラスは<compare>で定義されているので、使う場合にはインクルードしておきます。

#include <compare>
#include <iostream>

int a = 0, b = 1;
auto result = a <=> b;

if(result < 0){
    std::cout << "a < b" << std::endl;
}
else if(result > 0){
    std::cout << "a > b" << std::endl;
}
else if(result == 0){
    std::cout << "a == b" << std::endl;
}

独自クラスに対する三方比較演算子の定義

独自に作成したクラスに対して三方比較演算子を定義すると、比較演算子<, <=, >, >=が自動的に生成されます。また、三方比較演算子をdefault定義した場合、(すでに==が定義されていなければ)==default定義され、!===から自動生成されるため、最大6種類の比較演算子が自動的に生成されることとなります。

これにより、比較演算子の定義が容易となり、実装の手間が少なくなります。

#include <iostream>

class C
{
public:
    auto operator<=>(const C &) const = default;

    int a;
    char b;
};

int main()
{
    C c1{0, 'A'}, c2{1, 'B'}, c3{0, 'B'};

    std::cout << std::boolalpha;
    std::cout << (c1  < c2) << std::endl; // true
    std::cout << (c1 == c2) << std::endl; // false
    std::cout << (c1  < c3) << std::endl; // true

    return 0;
}

default定義した場合は、メンバ同士を順に比較します。

三方比較演算子をdefault定義しなかった場合、==は自動生成されないため、==も一緒に定義する必要があります。

比較カテゴリ型

三方比較演算子を独自に定義する場合、比較カテゴリ型のオブジェクトを戻り値とする必要があります。比較カテゴリ型のクラスは3種類あり、std::partial_ordering, std::weak_ordering, std::strong_orderingです。

それぞれ以下のような違いがあります。

  1. std::partial_ordering
    半順序。順序付け不能な値が存在する。
  2. std::weak_ordering
    弱順序。順序付け不能な値を同値として取り扱い比較可能とする。
  3. std::strong_ordering
    全順序。順序付け不能な要素がない。

三方比較演算子の定義例

独自に作成したクラスに三方比較演算子を定義した例を示します。Monthクラスは月を表すクラスで、Month::MonthId::JanuaryからMonth::MonthId::Decemberまでの12個の値を内部的に取り得ます。このクラスに対し、Monthクラスのオブジェクト同士、Month::MonthIdとの比較、符号なし整数との比較の3種類を実装してみました。

#include <compare>
#include <iostream>

class Month
{
public:
    enum class MonthId : unsigned int {
        January = 1, Febrary, March, April, May, June,
        July, August, September, October, November, December
    };

    Month() = delete;
    explicit Month(MonthId id) : m(id) {}

    auto operator<=>(const Month &) const = default;
    bool operator==(const Month &) const = default;   /* 明示的に定義する必要あり */

    std::strong_ordering operator<=>(const MonthId id) const
    {
        return m <=> id;
    }

    bool operator==(const MonthId id) const
    {
        return m == id;
    }

    std::partial_ordering operator<=>(const unsigned int v) const
    {
        if(0 < v && v < 13){
            return static_cast<unsigned int>(m) <=> v;
        }

        return std::partial_ordering::unordered;
    }

    bool operator==(const unsigned int v) const
    {
        return static_cast<unsigned int>(m) == v;
    }

private:
    MonthId m;
};

int main()
{
    Month a(Month::MonthId::January);
    Month b(Month::MonthId::August);

    std::cout << std::boolalpha;

    std::cout << (a < b) << std::endl;
    std::cout << (a == b) << std::endl;
    std::cout << (a > b) << std::endl << std::endl;

    std::cout << (a < Month::MonthId::January) << std::endl;
    std::cout << (a == Month::MonthId::January) << std::endl;
    std::cout << (a > Month::MonthId::January) << std::endl << std::endl;

    std::cout << (b < 6) << std::endl;
    std::cout << (b == 6) << std::endl;
    std::cout << (b > 6) << std::endl << std::endl;

    std::cout << (b < 13) << std::endl;
    std::cout << (b == 13) << std::endl;
    std::cout << (b > 13) << std::endl;

    return 0;
}

出力結果

true
false
false

false
true
false

false
false
true

false
false
false

Monthクラスのオブジェクト同士の比較では、メンバ同士の比較となるため、default実装で定義可能です。ただし==を定義しているため、==は自動的にはdefault実装されません。明示的にdefault実装する必要があります。

Month::MonthIdとの比較や符号なし整数との比較では、三方比較演算子を定義しているため、同時に==も定義する必要があります。Monthクラスのオブジェクトは、Month::MonthId::JanuaryからMonth::MonthId::Decemberまでの値しか取り得ないため、Month::MonthIdとの三方比較では全順序としてstd::strong_ordering型のオブジェクトを返すようにしました。

一方、符号なし整数との三方比較では、0や13といった月として存在しない数値が指定される可能性があるため、これらの数値が指定された場合に順序付け不能として、std::partial_ordering::unorderedを返すようにしました。三方比較演算子がstd::partial_ordering::unorderedを返した場合、比較演算子の結果はfalseとなります。

まとめ

C++20にて追加された新しい二項演算子である、三方比較演算子について調べてみました。まとめると以下になります。

  1. bool型ではなく、比較カテゴリ型を返す演算子
  2. 三方比較演算子をdefault実装すると、<, <=, >, >=, ==, !=の最大6種の比較演算子を自動生成する
  3. 三方比較演算子を独自実装する場合、==も実装する必要がある
  4. 比較カテゴリ型は以下の3種類
    1. std::partial_ordering
      半順序。順序付け不能な値が存在する。
    2. std::weak_ordering
      弱順序。順序付け不能な値を同値として取り扱い比較可能とする。
    3. std::strong_ordering
      全順序。順序付け不能な要素がない。

C++

Posted by izadori