web-dev-qa-db-ja.com

なぜrequireが必要なのですか?

C++ 20の概念のコーナーの1つは、requires requires。たとえば、この例は [expr.prim.req]/

requires-expressionは、アドホック制約を記述する方法として、requires-clause([temp])でも使用できます。以下のようなテンプレート引数について:

template<typename T>
  requires requires (T x) { x + x; }
    T add(T a, T b) { return a + b; }

最初の要求ではrequires-clauseが導入され、2番目の要求ではrequires-expressionが導入されます。

2番目のrequiresキーワードが必要な技術的理由は何ですか?なぜ書き込みを許可できないのですか:

template<typename T>
  requires (T x) { x + x; }
    T add(T a, T b) { return a + b; }

(注:文法requires itと答えないでください)

158
Barry

文法がそれを必要とするからです。します。

requires制約はhave torequires式を使用しません。多かれ少なかれ任意のブール定数式を使用できます。したがって、requires (foo)は正当なrequires制約でなければなりません。

requiresexpression(特定の制約が特定の制約に従っているかどうかをテストするもの)は、明確な構造です。同じキーワードで導入されたばかりです。 requires (foo f)は、有効なrequires式の始まりです。

必要なのは、制約を受け入れる場所でrequiresを使用する場合、requires句から「制約+式」を作成できることです。

だからここに質問があります:requires (foo)をrequire制約に適切な場所に置くと...これがrequire constraint制約+式ではなく、あなたが望むように?

このことを考慮:

void bar() requires (foo)
{
  //stuff
}

fooがタイプの場合、(foo)はrequire式のパラメーターリストであり、{}は関数の本体ではなく、そのrequires式の本体です。それ以外の場合、foorequires句の式です。

さて、コンパイラはfooが最初に何であるかを把握するだけだと言えます。しかし、C++ 本当には、トークンのシーケンスを解析する基本的な動作が、トークンを理解する前にコンパイラがそれらの識別子の意味を理解することを要求する場合、それを好まない。はい、C++は状況依存なので、これは起こります。しかし、委員会は可能な限りそれを避けることを好みます。

はい、それは文法です。

78
Nicol Bolas

状況はnoexcept(noexcept(...))とまったく同じです。確かに、これは良いことよりも悪いことのように聞こえますが、説明させてください。 :)あなたがすでに知っていることから始めましょう:

C++ 11には、「noexcept- clauses」と「noexcept- expressions」があります。彼らは異なることをします。

  • noexcept- clauseには、「この関数はnoexcept when ...(何らかの条件)であるべきです」と書かれています。関数宣言に進み、ブール値のパラメーターを取り、宣言された関数の動作を変更します。

  • noexcept- expressionは、「コンパイラ、は、(一部の式)がnoexceptであるかどうかを教えてください」と言います。それ自体がブール式です。プログラムの動作に「副作用」はありません。コンパイラーにyes/noの質問に対する答えを求めるだけです。 「この表現は例外ではありませんか?」

できますnoexcept- clause内にnoexcept- expressionをネストしますが、通常、そうするのはスタイルが悪いと考えています。

template<class T>
void incr(T t) noexcept(noexcept(++t));  // NOT SO HOT

型特性にnoexcept- expressionをカプセル化する方が、より良いスタイルと見なされます。

template<class T> inline constexpr bool is_nothrow_incrable_v =
    noexcept(++std::declval<T&>());  // BETTER, PART 1

template<class T>
void incr(T t) noexcept(is_nothrow_incrable_v<T>);  // BETTER, PART 2

C++ 2a Working Draftには、「requires- clauses」と「requires- expressions」があります。彼らは異なることをします。

  • requires- clauseには、「この関数は次の場合にオーバーロード解決に参加する必要があります...(何らかの条件)」。関数宣言に進み、ブール値のパラメーターを取り、宣言された関数の動作を変更します。

  • requires- expressionは、「コンパイラ、(式のセット)が整形式かどうかを教えてください」と言います。それ自体がブール式です。プログラムの動作に「副作用」はありません。コンパイラーにyes/noの質問に対する答えを求めるだけです。 「この表現は整形式ですか?」

できますrequires- clause内にrequires- expressionをネストしますが、通常、そうするのはスタイルが悪いと考えています。

template<class T>
void incr(T t) requires (requires(T t) { ++t; });  // NOT SO HOT

型特性にrequires- expressionをカプセル化する方が良いスタイルと考えられます...

template<class T> inline constexpr bool is_incrable_v =
    requires(T t) { ++t; };  // BETTER, PART 1

template<class T>
void incr(T t) requires is_incrable_v<T>;  // BETTER, PART 2

...または(C++ 2a Working Draft)コンセプト。

template<class T> concept Incrable =
    requires(T t) { ++t; };  // BETTER, PART 1

template<class T>
void incr(T t) requires Incrable<T>;  // BETTER, PART 2
59
Quuxplusone

cppreferenceの概念ページ がこれを説明していると思います。 「数学」で説明できるので、なぜこれがこのようにならなければならないのかと言うと:

概念を定義したい場合、これを行います:

_template<typename T>
concept Addable = requires (T x) { x + x; }; // requires-expression
_

その概念を使用する関数を宣言する場合、これを行います。

_template<typename T> requires Addable<T> // requires-clause, not requires-expression
T add(T a, T b) { return a + b; }
_

概念を個別に定義したくない場合、あなたがしなければならないのは、何らかの置換だけだと思います。この部分requires (T x) { x + x; };を取り、_Addable<T>_部分を置き換えると、以下が得られます:

_template<typename T> requires requires (T x) { x + x; }
T add(T a, T b) { return a + b; }
_

これがあなたが尋ねていることです。

15

コメント Andrew Sutton(gccで実装したConceptsの作成者の1人)がこの点で非常に役立つことがわかったので、ここでその全体を引用したいと思いました:

少し前まで、requires-expressions(2番目のrequireで導入されたフレーズ)はconstraint-expressions(最初のrequiresで導入されたフレーズ)で許可されていませんでした。概念の定義にのみ表示されます。実際、これはまさにその主張が現れるその論文のセクションで提案されているものです。

ただし、2016年には、その制限を緩和する提案がありました[編集者注: P0266 ]。ペーパーのセクション4のパラグラフ4の取り消し線に注意してください。このように生まれたのに必要なものです。

実を言うと、私は実際にGCCでその制限を実装したことはなかったので、常に可能でした。ウォルターはそれを発見し、それを有用であると考え、その論文に導いたと思う。

私が二度書く必要があることに私が敏感でなかったと誰も考えないように、私はそれが単純化できるかどうかを決定しようとして少し時間を費やしました。短い答え:いいえ。

問題は、テンプレートパラメータリストの後に導入する必要がある2つの文法的な構成要素があることです。非常に一般的には、制約式(_P && Q_など)と構文要件(requires (T a) { ... }など)です。それはrequire-expressionと呼ばれます。

最初の要件は制約を導入します。 2番目のrequireはrequires-expressionを導入します。それが文法の作り方です。混乱することはまったくありません。

私は、ある時点で、これらを単一の要件にまとめようとしました。残念ながら、それはいくつかの非常に難しい解析問題につながります。たとえば、requiresの後の_(_がネストされた部分式またはパラメーターリストを示しているかどうかは簡単にはわかりません。これらの構文の完全な曖昧性の解消はないと思います(統一された初期化構文の理論的根拠を参照してください。この問題もあります)。

そこで、選択を行います。makeには、現在のように式を導入するか、パラメータ化された要件のリストを導入する必要があります。

ほとんどの場合(ほぼ100%の場合)、requires-expression以外のものが必要なため、現在のアプローチを選択しました。また、非常にまれなケースでは、アドホックな制約のためにrequire-expressionが必要でしたが、Wordを2回書くことは本当に気になりません。これは、テンプレートの十分に適切な抽象化を開発していないことを示す明らかな指標です。 (私が持っていた場合は、名前が付けられるためです。)

私はrequireにrequires-expressionを導入することを選択できました。実際には、すべての制約が次のように見えるようになるため、それは実際には悪化します。

_template<typename T>
  requires { requires Eq<T>; }
void f(T a, T b);
_

ここでは、2番目の要件はネスト要件と呼ばれます。式を評価します(requires-expressionのブロック内の他のコードは評価されません)。これは現状よりもずっと悪いと思います。これで、どこでも2回の書き込みが必要になります。

さらにキーワードを使用することもできます。これはそれ自体が問題であり、自転車の脱落だけではありません。重複を避けるためにキーワードを「再配布」する方法があるかもしれませんが、私はその深刻な考えを与えていません。しかし、それは実際に問題の本質を変えるものではありません。

10
Barry