Warum benötigen wir Anforderungen erfordert?

161

Eine der Ecken von C ++ 20-Konzepten ist, dass es bestimmte Situationen gibt, in denen Sie schreiben müssen requires requires. Zum Beispiel dieses Beispiel aus [expr.prim.req] / 3 :

Ein Requires-Ausdruck kann auch in einer Requires-Klausel ([temp]) verwendet werden, um Ad-hoc-Einschränkungen für Vorlagenargumente wie das folgende zu schreiben:

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

Das erste erfordert die Einführung der require-Klausel und das zweite die Einführung des require-Ausdrucks .

Was ist der technische Grund für die Notwendigkeit dieses zweiten requiresSchlüsselworts? Warum können wir nicht einfach schreiben lassen:

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

(Hinweis: Bitte antworten Sie nicht auf die Grammatik requires)

Barry
quelle
25
Vorschlag: "Gibt es etwas, was erfordert, erfordert?". Im Ernst, ich habe die Vermutung, dass es der gleiche Grund dafür ist noexcept(noexcept(...)).
Quentin
11
Die beiden requiressind meiner Meinung nach Homonyme: Sie sehen gleich aus, buchstabieren gleich, riechen gleich, unterscheiden sich aber grundsätzlich. Wenn ich eine Lösung vorschlagen würde, würde ich vorschlagen, eine davon umzubenennen.
YSC
5
@YSC - co_requires? (Entschuldigung, konnte nicht widerstehen).
Geschichtenerzähler - Unslander Monica
122
Wo wird der Wahnsinn aufhören? Das nächste, was Sie wissen, werden wir haben long long.
Eljay
8
@StoryTeller: "Benötigt erfordert erforderlich?" wäre noch alliterativer gewesen !!
PW

Antworten:

81

Es ist, weil die Grammatik es erfordert. Es tut.

Eine requiresEinschränkung nicht haben einen verwenden requiresAusdruck. Es kann jeden mehr oder weniger willkürlichen booleschen konstanten Ausdruck verwenden. Daher requires (foo)muss eine legitime requiresEinschränkung sein.

Ein requires Ausdruck (das Ding, das prüft, ob bestimmte Dinge bestimmten Einschränkungen folgen) ist ein bestimmtes Konstrukt; Es wird nur mit demselben Schlüsselwort eingeführt. requires (foo f)wäre der Anfang eines gültigen requiresAusdrucks.

Was Sie wollen, ist, dass Sie, wenn Sie requiresan einem Ort verwenden, der Einschränkungen akzeptiert, in der Lage sein sollten, aus der requiresKlausel einen "Constraint + Ausdruck" zu machen .

Hier ist also die Frage: Wenn Sie an requires (foo)einen Ort setzen, der für eine erforderliche Einschränkung geeignet ist ... wie weit muss der Parser gehen, bevor er erkennen kann, dass dies eine erforderliche Einschränkung ist und nicht eine Einschränkung + Ausdruck, wie Sie es möchten sein?

Bedenken Sie:

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

Wenn fooes sich um einen Typ handelt, (foo)handelt es sich um eine Parameterliste eines erforderlichen Ausdrucks, und alles in {}ist nicht der Hauptteil der Funktion, sondern der Hauptteil dieses requiresAusdrucks. Andernfalls fooist ein Ausdruck in einer requiresKlausel.

Nun, man könnte sagen, dass der Compiler nur herausfinden sollte, was foozuerst kommt. Aber C ++ mag es wirklich nicht, wenn der Compiler beim Parsen einer Folge von Token herausfinden muss, was diese Bezeichner bedeuten, bevor er einen Sinn für die Token ergibt. Ja, C ++ ist kontextsensitiv, dies geschieht also. Der Ausschuss zieht es jedoch vor, dies nach Möglichkeit zu vermeiden.

Also ja, es ist Grammatik.

Nicol Bolas
quelle
2
Ist es sinnvoll, eine Parameterliste mit einem Typ, aber ohne Namen zu haben?
NathanOliver
3
@ Quentin: Es gibt sicherlich Fälle von Kontextabhängigkeit in der C ++ - Grammatik. Aber das Komitee versucht wirklich, das zu minimieren, und sie mögen es definitiv nicht, mehr hinzuzufügen .
Nicol Bolas
3
@RobertAndrzejuk: Wenn requiresnach einer <>Reihe von Vorlagenargumenten oder nach einer Funktionsparameterliste angezeigt wird, handelt es sich um eine require -Klausel. Wenn dort requiresangezeigt wird, wo ein Ausdruck gültig ist, handelt es sich um einen erforderlichen Ausdruck. Dies kann durch die Struktur des Analysebaums bestimmt werden, nicht durch den Inhalt des Analysebaums (die Besonderheiten, wie ein Bezeichner definiert wird, sind der Inhalt des Baums).
Nicol Bolas
6
@RobertAndrzejuk: Sicher, der Requires-Ausdruck hätte ein anderes Schlüsselwort verwenden können. Schlüsselwörter verursachen in C ++ jedoch enorme Kosten, da sie jedes Programm beschädigen können, das die Kennung verwendet, die zu einem Schlüsselwort geworden ist. Mit dem Konzeptvorschlag wurden bereits zwei Schlüsselwörter eingeführt: conceptund requires. Die Einführung eines dritten, bei dem der zweite beide Fälle ohne grammatikalische Probleme und mit wenigen Problemen für den Benutzer abdecken könnte, ist nur verschwenderisch. Schließlich besteht das einzige visuelle Problem darin, dass das Schlüsselwort zufällig zweimal wiederholt wird.
Nicol Bolas
3
@RobertAndrzejuk Es ist sowieso eine schlechte Praxis, solche Einschränkungen inline zu setzen, da Sie keine Subsumtion erhalten, als hätten Sie ein Konzept geschrieben. Es ist daher keine gute Idee, eine Kennung für eine so geringe, nicht empfohlene Funktion zu verwenden.
Rakete1111
60

Die Situation ist genau analog zu noexcept(noexcept(...)). Sicher, das klingt eher nach einer schlechten als nach einer guten Sache, aber lassen Sie mich das erklären. :) Wir beginnen mit dem, was Sie bereits wissen:

C ++ 11 hat " noexcept-clauses" und "-expressions" noexcept. Sie machen verschiedene Dinge.

  • Eine noexceptKlausel sagt: "Diese Funktion sollte nicht außer ... (eine Bedingung) sein." Es geht auf eine Funktionsdeklaration, nimmt einen booleschen Parameter und verursacht eine Verhaltensänderung in der deklarierten Funktion.

  • Ein Ausdruck noexceptsagt: "Compiler, bitte sagen Sie mir, ob (irgendein Ausdruck) keine Ausnahme ist." Es ist selbst ein boolescher Ausdruck. Es hat keine "Nebenwirkungen" auf das Verhalten des Programms - es fragt nur den Compiler nach der Antwort auf eine Ja / Nein-Frage. "Ist dieser Ausdruck keine Ausnahme?"

Wir können Nest einen noexcept-Ausdrucks in einem noexcept-Klausel, aber wir es in der Regel schlechten Stil betrachten , dies zu tun.

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

Es wird als besserer Stil angesehen, den noexceptAusdruck in ein Typmerkmal zu kapseln .

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

Der C ++ 2a Working Draft enthält " requires-clauses" und "-expressions" requires. Sie machen verschiedene Dinge.

  • Eine requiresKlausel besagt: "Diese Funktion sollte an der Überlastungsauflösung teilnehmen, wenn ... (eine Bedingung)." Es geht auf eine Funktionsdeklaration, nimmt einen booleschen Parameter und verursacht eine Verhaltensänderung in der deklarierten Funktion.

  • Ein requiresAusdruck sagt: "Compiler, bitte sagen Sie mir, ob (einige Ausdrücke) wohlgeformt sind." Es ist selbst ein boolescher Ausdruck. Es hat keine "Nebenwirkungen" auf das Verhalten des Programms - es fragt nur den Compiler nach der Antwort auf eine Ja / Nein-Frage. "Ist dieser Ausdruck wohlgeformt?"

Wir können Nest einen requires-Ausdrucks in einem requires-Klausel, aber wir es in der Regel schlechten Stil betrachten , dies zu tun.

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

Es wird als besserer Stil angesehen, den requiresAusdruck in ein Typmerkmal zu kapseln ...

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

... oder in einem (C ++ 2a Working Draft) Konzept.

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
Quuxpluson
quelle
1
Ich kaufe dieses Argument nicht wirklich. noexcepthat das Problem , dass noexcept(f())könnte bedeuten , entweder interpretieren f()als boolean , dass wir die Spezifikation festgelegt verwenden oder zu prüfen , ob oder nicht f()ist noexcept. requireshat diese Mehrdeutigkeit nicht, da die Ausdrücke, deren Gültigkeit überprüft wird, bereits mit {}s eingeführt werden müssen. Danach lautet das Argument im Grunde "die Grammatik sagt es".
Barry
@ Barry: Siehe diesen Kommentar . Es scheint, dass die {}optional sind.
Eric
1
@Eric Die {}sind nicht optional, das zeigt dieser Kommentar nicht. Dies ist jedoch ein großartiger Kommentar, der die Mehrdeutigkeit beim Parsen demonstriert. Würde diesen Kommentar (mit einigen Erklärungen) wahrscheinlich als eigenständige Antwort akzeptieren
Barry
1
requires is_nothrow_incrable_v<T>;sollte seinrequires is_incrable_v<T>;
Ruslan
16

Ich denke, die Konzeptseite von cppreference erklärt dies. Ich kann sozusagen mit "Mathe" erklären, warum das so sein muss:

Wenn Sie ein Konzept definieren möchten, gehen Sie folgendermaßen vor:

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

Wenn Sie eine Funktion deklarieren möchten, die dieses Konzept verwendet, gehen Sie folgendermaßen vor:

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

Wenn Sie das Konzept nicht separat definieren möchten, müssen Sie wahrscheinlich nur eine Substitution durchführen. Nehmen Sie dieses Teil requires (T x) { x + x; };und ersetzen Sie das Addable<T>Teil, und Sie erhalten:

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

Nach was fragst du?

Der Quantenphysiker
quelle
4
Ich glaube nicht, worum es bei der Frage geht. Dies erklärt mehr oder weniger die Grammatik.
Passant Bis zum
Aber warum können template<typename T> requires (T x) { x + x; }und verlangen Sie nicht , dass erfordern sowohl die Klausel als auch der Ausdruck sein kann?
NathanOliver
2
@ NathanOliver: Weil Sie den Compiler zwingen, ein Konstrukt als ein anderes zu interpretieren. Eine requires-as-Beschränkungsklausel nicht müssen sein , requires-Ausdrucks. Das ist nur eine mögliche Verwendung davon.
Nicol Bolas
2
@TheQuantumPhysicist Was ich mit meinem Kommentar erreicht habe, ist, dass diese Antwort nur die Syntax erklärt. Nicht welchen tatsächlichen technischen Grund wir haben müssen requires requires. Sie hätten der Grammatik etwas hinzufügen können, um dies zuzulassen, template<typename T> requires (T x) { x + x; }aber sie taten es nicht. Barry will wissen, warum sie es nicht getan haben
NathanOliver
3
Wenn wir hier wirklich spielen, um die Grammatik zu finden, werde ich beißen. godbolt.org/z/i6n8kM template<class T> void f(T) requires requires(T (x)) { (void)x; }; bedeutet etwas anderes, wenn Sie eines der requireses entfernen .
Quuxplusone
12

Ich fand einen Kommentar von Andrew Sutton (einem der Concepts-Autoren, der ihn in gcc implementiert hat) in dieser Hinsicht sehr hilfreich, daher dachte ich, ich würde ihn hier fast vollständig zitieren:

Vor nicht allzu langer Zeit waren Anforderungsausdrücke (der durch den zweiten eingeführte Ausdruck erfordert) in Einschränkungsausdrücken (der durch den ersten eingeführte Ausdruck erfordert) nicht zulässig. Es konnte nur in Konzeptdefinitionen erscheinen. Genau dies wird in dem Abschnitt dieses Papiers vorgeschlagen, in dem diese Behauptung erscheint.

Im Jahr 2016 gab es jedoch einen Vorschlag, diese Einschränkung zu lockern [Anmerkung des Herausgebers: P0266 ]. Beachten Sie den durchgestrichenen Absatz 4 in Abschnitt 4 des Papiers. Und so wurde geboren erfordert erfordert.

Um die Wahrheit zu sagen, ich hatte diese Einschränkung nie in GCC implementiert, also war es immer möglich gewesen. Ich denke, dass Walter das entdeckt und nützlich gefunden hat, was zu diesem Papier geführt hat.

Damit niemand glaubt, dass ich nicht empfindlich auf das Schreiben reagiere, habe ich einige Zeit damit verbracht, herauszufinden, ob dies vereinfacht werden kann. Kurze Antwort: nein.

Das Problem ist, dass nach einer Vorlagenparameterliste zwei grammatikalische Konstrukte eingeführt werden müssen: sehr häufig ein Einschränkungsausdruck (wie P && Q) und gelegentlich syntaktische Anforderungen (wie requires (T a) { ... }). Das nennt man einen Anforderungsausdruck.

Das erste erfordert die Einführung der Einschränkung. Die zweite Anforderung führt den erforderlichen Ausdruck ein. So komponiert die Grammatik. Ich finde es überhaupt nicht verwirrend.

Ich habe einmal versucht, diese auf eine einzige Anforderung zu reduzieren. Leider führt dies zu einigen ernsthaft schwierigen Analyseproblemen. Sie können beispielsweise nicht leicht erkennen, ob a (after the require einen verschachtelten Unterausdruck oder eine Parameterliste bezeichnet. Ich glaube nicht, dass es eine perfekte Disambiguierung dieser Syntax gibt (siehe die Begründung für eine einheitliche Initialisierungssyntax; dieses Problem gibt es auch).

Sie treffen also eine Wahl: make erfordert die Einführung eines Ausdrucks (wie jetzt) ​​oder die Einführung einer parametrisierten Liste von Anforderungen.

Ich habe mich für den aktuellen Ansatz entschieden, weil ich die meiste Zeit (wie in fast 100% der Fälle) etwas anderes als einen erforderlichen Ausdruck möchte. Und in dem äußerst seltenen Fall, dass ich einen Anforderungsausdruck für Ad-hoc-Einschränkungen wollte, macht es mir wirklich nichts aus, das Wort zweimal zu schreiben. Es ist ein offensichtlicher Indikator dafür, dass ich keine ausreichend solide Abstraktion für die Vorlage entwickelt habe. (Denn wenn ich es hätte, hätte es einen Namen.)

Ich hätte mich dafür entscheiden können, dass die Anforderungen einen Anforderungen-Ausdruck einführen. Das ist tatsächlich schlimmer, weil praktisch alle Ihre Einschränkungen so aussehen würden:

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

Hier wird die zweite Anforderung als verschachtelte Anforderung bezeichnet. es wertet seinen Ausdruck aus (anderer Code im Block des erforderlichen Ausdrucks wird nicht ausgewertet). Ich denke, das ist viel schlimmer als der Status Quo. Jetzt müssen Sie zweimal überall schreiben.

Ich hätte auch mehr Schlüsselwörter verwenden können. Dies ist ein eigenständiges Problem - und es ist nicht nur ein Fahrradschuppen. Es könnte eine Möglichkeit geben, Schlüsselwörter "neu zu verteilen", um die Duplizierung zu vermeiden, aber ich habe nicht ernsthaft darüber nachgedacht. Aber das ändert nichts an der Essenz des Problems.

Barry
quelle
-10

Weil Sie sagen, dass eine Sache A eine Anforderung B hat und die Anforderung B eine Anforderung C.

Das Ding A benötigt B, was wiederum C erfordert.

Die "require" -Klausel selbst erfordert etwas.

Sie haben Ding A (erfordert B (erfordert C)).

Meh. :) :)

Leichtigkeitsrennen im Orbit
quelle
4
Aber nach den anderen Antworten sind die erste und die zweite requireskonzeptionell nicht dasselbe (eine ist eine Klausel, eine ein Ausdruck). Wenn ich das richtig verstehe, haben die beiden Sätze von ()in requires (requires (T x) { x + x; })sehr unterschiedliche Bedeutungen (die äußere ist optional und enthält immer einen booleschen Zusammenhang; die innere ist ein obligatorischer Bestandteil der Einführung eines erforderlichen Ausdrucks und lässt keine tatsächlichen Ausdrücke zu).
Max Langhof
2
@ MaxLanghof Wollen Sie damit sagen, dass die Anforderungen unterschiedlich sind? : D
Leichtigkeitsrennen im Orbit