C++20 では unique_ptr が equality_comparable_with nullptr_t ではないのはなぜですか? 質問する

C++20 では unique_ptr が equality_comparable_with nullptr_t ではないのはなぜですか? 質問する

C++20のconceptsを使っているとstd::unique_ptr、次の条件を満たしていないことに気付きました。std::equality_comparable_with<std::nullptr_t,...>コンセプト。std::unique_ptrの定義では、C++20 では以下を実装することになっています。

template<class T1, class D1, class T2, class D2>
bool operator==(const unique_ptr<T1, D1>& x, const unique_ptr<T2, D2>& y);

template <class T, class D>
bool operator==(const unique_ptr<T, D>& x, std::nullptr_t) noexcept;

この要件すべきとの対称比較を実装しますnullptr。私の理解では、これで を満たすのに十分ですequality_comparable_with

興味深いことに、この問題はすべての主要なコンパイラで一貫しているようです。次のコードは、Clang、GCC、および MSVC から拒否されます。

// fails on all three compilers
static_assert(std::equality_comparable_with<std::unique_ptr<int>,std::nullptr_t>);

Try Online

しかし、次のような同じ主張もstd::shared_ptr受け入れられます:

// succeeds on all three compilers
static_assert(std::equality_comparable_with<std::shared_ptr<int>,std::nullptr_t>);

Try Online

私が何かを誤解していない限り、これはバグのようです。私の質問は、これが 3 つのコンパイラ実装における偶然のバグなのか、それとも C++20 標準の欠陥なのかということです。

注記:これをタグ付けします万が一、欠陥があった場合に備えて。

ベストアンサー1

TL;DR:は、 と のstd::equality_comparable_with<T, U>両方TがとUの共通参照に変換可能であることを要求します。との場合、これは がコピー構築可能であることを要求しますが、これはそうではありません。TUstd::unique_ptr<T>std::nullptr_tstd::unique_ptr<T>


シートベルトを締めてください。これはかなりのドライブです。オタク狙撃

なぜそのコンセプトを満たさないのでしょうか?

std::equality_comparable_with必要:

template <class T, class U>
concept equality_comparable_with =
  std::equality_comparable<T> &&
  std::equality_comparable<U> &&
  std::common_reference_with<
    const std::remove_reference_t<T>&,
    const std::remove_reference_t<U>&> &&
  std::equality_comparable<
    std::common_reference_t<
      const std::remove_reference_t<T>&,
      const std::remove_reference_t<U>&>> &&
  __WeaklyEqualityComparableWith<T, U>;

長いですね。概念を各部分に分解すると、std::equality_comparable_with<std::unique_ptr<int>, std::nullptr_t>次のようになりますstd::common_reference_with<const std::unique_ptr<int>&, const std::nullptr_t&>

<source>:6:20: note: constraints not satisfied
In file included from <source>:1: 
/…/concepts:72:13:   required for the satisfaction of
    'convertible_to<_Tp, typename std::common_reference<_Tp1, _Tp2>::type>'
    [with _Tp = const std::unique_ptr<int, std::default_delete<int> >&; _Tp2 = const std::nullptr_t&; _Tp1 = const std::unique_ptr<int, std::default_delete<int> >&]
/…/concepts:72:30: note: the expression 'is_convertible_v<_From, _To>
    [with _From = const std::unique_ptr<int, std::default_delete<int> >&; _To = std::unique_ptr<int, std::default_delete<int> >]' evaluated to 'false'
   72 |     concept convertible_to = is_convertible_v<_From, _To>
      |                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~

(読みやすくするために編集されています)コンパイラエクスプローラーリンク

std::common_reference_with必要:

template < class T, class U >
concept common_reference_with =
  std::same_as<std::common_reference_t<T, U>, std::common_reference_t<U, T>> &&
  std::convertible_to<T, std::common_reference_t<T, U>> &&
  std::convertible_to<U, std::common_reference_t<T, U>>;

std::common_reference_t<const std::unique_ptr<int>&, const std::nullptr_t&>std::unique_ptr<int>(参照コンパイラエクスプローラーリンク)。

これをまとめると、 という推移的な要件があり、これは がコピー構築可能であるstd::convertible_to<const std::unique_ptr<int>&, std::unique_ptr<int>>ことを要求するのと同等です。std::unique_ptr<int>

なぜstd::common_reference_t参照ではないのですか?

なぜstd::common_reference_t<const std::unique_ptr<T>&, const std::nullptr_t&> = std::unique_ptr<T>ではなく なのでしょうかconst std::unique_ptr<T>&std::common_reference_t2 つのタイプ (sizeof...(T)は 2 つ) の場合、次のようになります。

  • T1T2両方とも参照型であり、単純な共通参照型 SおよびT1(T2以下に定義される) が存在する場合、メンバー型 type names S;
  • それ以外の場合、std::basic_common_reference<std::remove_cvref_t<T1>, std::remove_cvref_t<T2>, T1Q, T2Q>::typeが存在し、が の cv 修飾子と参照修飾子が追加された単項TiQエイリアス テンプレートである場合、メンバー型 type はその型の名前になります。TiQ<U>UTi
  • それ以外の場合、decltype(false? val<T1>() : val<T2>())(val は関数テンプレート )template<class T> T val();が有効な型である場合、メンバー型 type はその型の名前になります。
  • それ以外の場合、std::common_type_t<T1, T2>が有効な型である場合、メンバー型 type はその型の名前になります。
  • それ以外の場合、メンバー タイプはありません。

const std::unique_ptr<T>&およびはconst std::nullptr_t&、参照が共通の基本型にすぐに変換できない (つまり、 がfalse ? crefUPtr : crefNullptrT不正な形式である)ため、単純な共通参照型を持ちません。std::basic_common_referenceの特殊化はありませんstd::unique_ptr<T>。 3 番目のオプションも失敗しますが、 をトリガーしますstd::common_type_t<const std::unique_ptr<T>&, const std::nullptr_t&>

のためにstd::common_typestd::common_type<const std::unique_ptr<T>&, const std::nullptr_t&> = std::common_type<std::unique_ptr<T>, std::nullptr_t>、 なぜなら:

std::decayと の少なくとも 1 つに適用するとT1異なるT2型が生成され、メンバー型はstd::common_type<std::decay<T1>::type, std::decay<T2>::type>::typeが存在する場合は と同じ型の名前を指定します。存在しない場合は、メンバー型は存在しません。

std::common_type<std::unique_ptr<T>, std::nullptr_t>は実際に存在しますstd::unique_ptr<T>。これは参照が削除される理由です。


このようなケースをサポートするために標準を修正できますか?

これはP2404は、移動のみの型をサポートするために、、、std::equality_comparable_withおよびに変更を提案しています。std::totally_ordered_withstd::three_way_comparable_with

なぜこのような共通参照要件があるのか​​?

`equality_comparable_with` には `common_reference` が必要ですか?TCによる正当化(出典:3351 円共通参照要件の 15 ~ 16 ページは次のとおりequality_comparable_withです。

異なる型の 2 つの値が等しいとはどういう意味でしょうか。設計では、型間の等価性は、それらを共通 (参照) 型にマッピングすることによって定義されます (この変換は値を保持するために必要です)。

==概念から単純に期待される操作を要求するだけでは機能しません。理由は次のとおりです。

[I]tは持つことを許可するがt == ut2 == ut != t2

したがって、共通参照の要件は数学的な健全性のために存在し、同時に次の実装を可能にします。

using common_ref_t = std::common_reference_t<const Lhs&, const Rhs&>;
common_ref_t lhs = lhs_;
common_ref_t rhs = rhs_;
return lhs == rhs;

n3351 がサポートしていた C++0X の概念では、異種が存在しない場合、この実装は実際にはフォールバックとして使用されますoperator==(T, U)。C++20 の概念では、異種がoperator==(T, U)存在する必要があるため、この実装が使用されることはありません。

n3351 は、この種の異種等価性はすでに等価性の拡張であり、単一の型内でのみ厳密に数学的に定義されていることを表現していることに注意してください。実際、異種等価性演算を記述する場合、2 つの型が共通のスーパータイプを共有し、その共通型内で演算が行われると想定しています。

共通参照要件はこのケースをサポートできますか?

おそらく、 の共通参照要件はstd::equality_comparable厳しすぎるでしょう。重要なのは、数学的な要件は、この持ち上げられたものが等式である共通のスーパータイプが存在するということだけですoperator==が、共通参照要件が要求するのはより厳密なもので、さらに次のものも要求することです。

  1. 共通スーパータイプは、 を通じて取得されたものである必要がありますstd::common_reference_t
  2. 共通のスーパータイプを形成できなければならない参照両方のタイプに。

最初のポイントを緩和することは、基本的に、std::equality_comparable_withコンセプトを満たすために 2 つの型を明示的に選択できる明示的なカスタマイズ ポイントを提供するだけです。2 番目のポイントについては、数学的には「参照」は無意味です。したがって、この 2 番目のポイントも緩和して、共通のスーパータイプを両方の型から暗黙的に変換できるようにすることができます。

共通参照の要件を緩和して、意図された共通スーパータイプの要件にさらに厳密に従うことはできますか?

これを正しく行うのは困難です。重要なのは、共通スーパータイプが存在するかどうかだけを気にし、コード内で実際にそれを使用する必要がないことです。そのため、共通スーパータイプの変換をコード化する際に、効率性や実装が不可能かどうかについて心配する必要はありません。

std::common_reference_withこれは、次の部分を変更することで実現できますequality_comparable_with

template <class T, class U>
concept equality_comparable_with =
  __WeaklyEqualityComparableWith<T, U> &&
  std::equality_comparable<T> &&
  std::equality_comparable<U> &&
  std::equality_comparable<
    std::common_reference_t<
      const std::remove_reference_t<T>&,
      const std::remove_reference_t<U>&>> &&
  __CommonSupertypeWith<T, U>;

template <class T, class U>
concept __CommonSupertypeWith = 
  std::same_as<
    std::common_reference_t<
      const std::remove_cvref_t<T>&,
      const std::remove_cvref_t<U>&>,
    std::common_reference_t<
      const std::remove_cvref_t<U>&,
      const std::remove_cvref_t<T>&>> &&
  (std::convertible_to<const std::remove_cvref_t<T>&,
    std::common_reference_t<
      const std::remove_cvref_t<T>&,
      const std::remove_cvref_t<U>&>> ||
   std::convertible_to<std::remove_cvref_t<T>&&,
    std::common_reference_t<
      const std::remove_cvref_t<T>&,
      const std::remove_cvref_t<U>&>>) &&
  (std::convertible_to<const std::remove_cvref_t<U>&,
    std::common_reference_t<
      const std::remove_cvref_t<T>&,
      const std::remove_cvref_t<U>&>> ||
   std::convertible_to<std::remove_cvref_t<U>&&,
    std::common_reference_t<
      const std::remove_cvref_t<T>&,
      const std::remove_cvref_t<U>&>>);

特に、変更は、または の参照を取り除いたバージョンを作成できるようにすることで、 と の両方を試して共通の参照を作成することによって、 が異なるという仮定に変更common_reference_withされます。詳細については、__CommonSupertypeWith__CommonSupertypeWithstd::common_reference_t<T, U>TUC(T&&)C(const T&)P2404


std::equality_comparable_withこれが標準に統合される前に、どのように回避すればよいでしょうか?

使用するオーバーロードを変更する

std::equality_comparable_with標準ライブラリの(または他の概念)のあらゆる使用には*_with、関数を渡すことができる述語オーバーロードが便利です。つまり、std::equal_to()述語オーバーロードに渡すだけで、必要な動作を得ることができます(ない std::ranges::equal_toは制約付きですが、制約なしの は制約付きではありませんstd::equal_to

ただし、これは修正しないのが良い考えであるという意味ではありませんstd::equality_comparable_with

を満たすために独自のタイプを拡張できますかstd::equality_comparable_with?

共通参照要件は を使用しますstd::common_reference_t。これはカスタマイズポイントが です。std::basic_common_reference、 ために:

クラス テンプレートは、ユーザーがユーザー定義型 (通常はプロキシ参照)basic_common_referenceの結果に影響を与えることができるカスタマイズ ポイントです。common_reference

これはひどいハックですが、比較したい両方の型をサポートするプロキシ参照を記述すれば、std::basic_common_reference型を特殊化して、型が を満たすことができますstd::equality_comparable_withMyCustomType が SomeOtherType と equality_comparable_with であることをコンパイラーに伝えるにはどうすればよいでしょうか?これを行う場合は注意してください。 がまたは他の概念std::common_reference_tによってのみ使用されない場合は、将来的に連鎖的な問題が発生するリスクがあります。共通参照が実際に共通参照であることを確認することをお勧めします。例:std::equality_comparable_withcomparison_relation_with

template <typename T>
class custom_vector { ... };

template <typename T>
class custom_vector_ref { ... };

custom_vector_ref<T>custom_vector<T>は、との間custom_vector_ref<T>、あるいは と の間のcustom_vector<T>共通の参照に適したオプションである可能性がありますstd::array<T, N>。慎重に進めてください。

自分が制御していない型を拡張するにはどうすればいいでしょうかstd::equality_comparable_with?

できません。std::basic_common_reference所有していない型 (std::型またはサードパーティのライブラリ) に特化することは、良くても悪い習慣であり、最悪の場合、未定義の動作です。最も安全な選択は、比較できる所有するプロキシ型を使用するか、std::equality_comparable_with同等性のカスタム スペルの明示的なカスタマイズ ポイントを持つ独自の拡張機能を作成することです。


さて、これらの要件の考え方は数学的な健全性であることは理解していますが、これらの要件はどのようにして数学的な健全性を達成するのでしょうか。また、それがなぜそれほど重要なのでしょうか。

数学的には、等式は同値関係です。ただし、同値関係は単一の集合に対して定義されます。では、2 つの集合Aとの間の同値関係をどのように定義できるでしょうBか。簡単に言うと、代わりに に対して同値関係を定義しますC = A∪B。つまり、Aとの共通スーパータイプを取りB、このスーパータイプに対して同値関係を定義します。

これは、と がどこから来ようc1 == c2とも関係を定義する必要があることを意味し、 、、(は から来ており、 はから来ている)がなければなりません。 C++ に翻訳すると、 、、のすべてが同じ等式の一部でなければならないことを意味します。c1c2a1 == a2a == bb1 == b2aiAbiBoperator==(A, A)operator==(A, B)operator==(B, B)operator==(C, C)

これが、iterator/sentinelが を満たさない理由ですstd::equality_comparable_with。 はoperator==(iterator, sentinel)実際には何らかの同値関係の一部である可能性がありますが、 と同じ同値関係の一部ではありませんoperator==(iterator, iterator)(そうでない場合、反復子の等価性は、「両方の反復子が末尾にあるか、両方の反復子が末尾にないか」という質問にのみ答えます)。

実際には等式ではないを書くのは実は非常に簡単ですoperator==。なぜなら、異種等式はoperator==(A, B)あなたが書いている単一の ではなく、operator==すべてがまとまりを持つ 4 つの異なる であることを覚えておく必要があるからです。

ちょっと待ってください。なぜ 4 つの s がすべて必要なのでしょうか。最適化のために、operator==と だけで済ませられないのはなぜでしょうか。operator==(C, C)operator==(A, B)

これは有効なモデルであり、これを行うことができます。ただし、C++ はプラトン的な現実ではありません。概念は、意味要件を本当に満たす型のみを受け入れるように全力を尽くしますが、実際にこの目標を達成することはできません。そのため、とのみをチェックするとoperator==(A, B)、とが何か異なることをoperator==(C, C)行うリスクがあります。さらに、 が得られる場合、にあるものに基づいてと を書くのは簡単です。つまり、とを要求することによる害は非常に低く、その代わりに、実際に等式があることに対する確信が高まります。operator==(A, A)operator==(B, B)operator==(C, C)operator==(A, A)operator==(B, B)operator==(C, C)operator==(A, A)operator==(B, B)

しかし、状況によっては、これがうまくいかないこともあります。P2405

operator==(A, B)なんて疲れるんでしょう。 が実際に等しいことを要求するだけでいいのではないでしょうか。いずれにせよ、私は実際にoperator==(A, A)または を使用するつもりはありoperator==(B, B)ません。タイプ間の比較ができることだけを気にしていました。

実際、 が実際の等式であることを要求するモデルは、operator==(A, B)おそらく機能するでしょう。このモデルでは、 となりますstd::equality_comparable_with<iterator, sentinel>が、それがすべての既知のコンテキストで正確に何を意味するかは解明できません。ただし、これが標準で採用されなかった理由があり、変更するかどうか、または変更する方法が理解される前に、まず標準のモデルが選択された理由を理解する必要があります。

おすすめ記事