QtにおけるC++20比較演算子 (C++17でも使える!)

本稿は「C++20 comparison in Qt (even with C++17🤩)」の抄訳です。
 

Qt 6.7リリースでは、C++20の比較サポートを有効化し、その機能の一部をC++17でも使えるようにバックポートしました。本ブログ記事では、Qtが活用している比較機能強化の概要を説明し、カスタムクラスにそれらを実装する方法についてご案内します。

要約

C++20では、いくつかの新機能と改善が導入されました。特に注目すべきは、比較演算子の動作がいくつかの点で変更され、より少ないコードでより多くの処理を実現できるようになりました。

重要な追加機能は、3方向比較演算子 <=>宇宙船演算子とも呼ばれる)です。これは、単一の比較操作を実行し、左辺(LHS)が右辺(RHS)より小さいか、等しいか、大きいかを表す結果を返します。

宇宙船演算子の結果の種類は3つあり、比較される型で実装される強さのカテゴリーに応じて、std::strong_orderingstd::weak_ordering、またはstd::partial_orderingとなります。 これらの3つの順序カテゴリーは、単純なブール値の結果よりも、値を比較する際により微細な方法を提示します。なぜなら、これらのカテゴリーは、等価だけでなく、値間の相対的な関係も表現するからです。

  • 強い順序付け(strong ordering)では、等しいとみなされた値は、すべての通常の操作において同一の動作を保証します。つまり、値のすべての側面が、メモリアドレスを除いて同一であることを意味します。
  • 弱い順序付け(weak ordering)では、特定の点で異なるものの、いくつかの値を同等とみなします。例えば、「Qt」と「qt」という2つの文字列があるとします。大文字小文字を区別せずに比較すると、これらは弱い順序付けとみなされます。なぜでしょうか?LHS[0]とRHS[0]がまったく同じではない(Qとq)にもかかわらず、文字列は同等とみなされるからです。おまけ情報:このタイプは整列に適しています。
  • 部分順序(partial ordering)はより緩やかです。浮動小数点数のNaNを比較する場合のように、一部の値がまったく意味のある順序付けができない場合に便利です。そのような場合、結果はpartial_order::unorderedとなる可能性があります。

もう一つの注目すべき機能は、すでに定義されているものから不足している関係演算子を合成できることです。例えば、operator!=()operator==()から利用可能になり、他のすべての関係演算子はoperator<=>()から利用可能になります。

さらに、混合型比較では、コンパイラが逆順の演算子も合成するため、宇宙船演算子と等価演算子の両方において、引数の順序は問題になりません。

最終的には、このため、自分の型に対して <、<=、==、!=、>=、および > 演算子をすべて手動で実装する必要がなくなるため、作業が大幅に簡素化されます。代わりに、operator==() および operator<=>() の実装のみが必要となります。

QtにおけるC++20の比較サポート

ご存知かもしれませんが、Qt 6.7 は C++17 をサポートしていますが、C++20 でも使用できます。この点を踏まえ、両方のバージョンを使用する Qt ユーザーのために、C++20 の比較関連の機能の一部を利用できるようにしました。

まず、3`方向比較用のQt::strong_orderingQt::weak_orderingQt::partial_orderingという独自のQtの比較型を用意しました。これらは、C++20標準の順序型をC++17に逆移植したものです。

次に、<QtCore/QtCompare>にいくつかのヘルパーメソッドを導入しました。これにより、C++17でカスタム型に対して3方向比較を実装できるようになります。特に、Qt::compareThreeWay()は、組み込み型用の演算子<=>()のC++17版です。テンプレート型については、qCompareThreeWay()を提供しています。さらに詳しく説明すると、次のようになります。

Qt::compareThreeWay()

このヘルパー関数は、組み込みの C++ タイプに対して 3方向比較を提供します。C++20 サポート付きでコンパイルされた場合、Qt::compareThreeWay() は直接宇宙船演算子を呼び出します。C++17 を使用している場合は、この関数は比較を内部で処理し、Qt の比較型のいずれかを返すことで、宇宙船演算子の動作を模倣します。

Qt::compareThreeWay() を使用してポインタの比較を行う場合は、Qt 6.8 で導入された Qt::totally_ordered_wrapper にラップする必要があることに注意してください。そうしないと、予期しない動作が発生する可能性があります。その予期しない動作について興味がある方は、「組み込みのポインタ相対比較」のセクションをお読みください。

qCompareThreeWay()

これは3方向比較を行うテンプレート関数です。比較される型の情報を何も知らない場合の汎用コードに役立ちます。ただし、これは (LeftType, RightType) ペアまたは (RightType, LeftType) の逆順ペアに対して compareThreeWay() が実装されている場合のみ利用可能です。そのため、まず最初に、自分の型に対して compareThreeWay() ヘルパー関数を用意してください。


Qt 6.7では、QDateQTimeQDateTimeQTimeZoneqfloat16クラスが更新され、比較演算子が近代化されました。Qt 6.8では、Qt CoreクラスのほとんどがC++20比較演算子に対応しました。

これは、クラス定義内に2つのヘルパー関数を提供することで実現されました。

1.

friend bool comparesEqual(LeftType lhs, RightType rhs)

comparesEqual() は、C++20 における operator==() および C++17 における operator!=() の実装に使用されます。

2.

friend ReturnType compareThreeWay(LeftType lhs, RightType rhs);

compareThreeWay() は、C++17 の 4 つの関係演算子、または C++20 の operator<=>() を実装するために使用されます。ReturnType は、Qt の比較型のいずれかでなければなりません。

実装例

以下は3方向比較を実装する2つの例を見ていきます。これにより、比較を近代化する際に、どのヘルパー関数が最も適しているかを明確になります。

Qt::compareThreeWay() の例

単純化のため、ここではintfloatのメンバー変数を持つMyWrapperというクラスを1つ取り上げます。まず、独自のヘルパー関数compareThreeWay()を用意します。その後、新たに作成したcompareThreeWay()の結果に基づいて関係演算子を作成します。operator==()用のis_eq()やoperator<()用のis_lt()など、名前付き比較関数のQtバージョンを活用してください。これらは、3方向比較の結果を関係演算子の結果に変換します。これらはC++20で導入されたものですが、C++17でも実装しています。

class MyWrapper {

public:
    constexpr MyWrapper(int int_val, float float_val): m_i(int_val), m_f(float_val) {}

private:
  friend Qt::partial_ordering
compareThreeWay(const MyWrapper &lhs, const MyWrapper &rhs) noexcept
  {
      auto int_res = Qt::compareThreeWay(lhs.m_i, rhs.m_i);
      if (is_neq(int_res))
          return int_res;
      auto float_res = Qt::compareThreeWay(lhs.m_f, rhs.m_f);
      return float_res;
  }

friend bool operator==(const MyWrapper &lhs, const MyWrapper &rhs) noexcept
{ return is_eq(compareThreeWay(lhs, rhs)); }
   
friend bool operator<(const MyWrapper &lhs, const MyWrapper &rhs) noexcept
{ return is_lt(compareThreeWay(lhs, rhs)); }


// same for other relational operators, if needed

  int m_i = 0;
  float m_f = 0;
};

int_res 型は Qt::strong_ordering ですが、float_res 型は Qt::partial_ordering であることに注目してください。そのため、compareThreeWay() の戻り値の型は Qt::partial_ordering でなければなりません。

qCompareThreeWay() の例

前述の通り、qCompareThreeWay() は主に汎用コードの型比較に役立ちます。 次のようなテンプレートクラスがあると仮定し、このクラスの2つのインスタンスを2つのメンバー変数に基づいて比較する必要があるとします。

template <typename Type1, typename Type2>
class TemplateClass
{
public:
Type1 t1;
Type2 t2;

   auto compareThreeWay(const TemplateClass &lhs, const TemplateClass &rhs)
{
const auto res = qCompareThreeWay(lhs.t1, rhs.t1);
if (is_neq(res))
return res;
return qCompareThreeWay(lhs.t2, rhs.t2);
}
}

テンプレートタイプがC++組み込み型の場合、Qt::compareThreeWay()が呼び出されることに留意してください。それ以外のカスタム型の場合は、そのカスタム型にcompareThreeWay()という名前のフレンド関数がすでに実装されている必要があります。

注:qCompareThreeWay() および compareThreeWay() 関数の想定される戻り値については、Qt::strong_orderingQt::weak_orderingQt::partial_orderingを参照してください。

まとめ

ここまでお読みいただきありがとうございます。前述の通り、今後のQtリリースでは、より多くのQt CoreクラスがC++20の比較をサポートする予定です。Qt::compareThreeWay()またはqCompareThreeWay()が適用可能な場合は、ぜひご自身のクラスで比較の近代化をお試しください。

このブログ記事がお役に立てば幸いです。ご意見は以下のコメント欄にお寄せください!


Blog Topics:

Comments