Pourquoi exige-t-on des exigences?

161

L'un des angles des concepts C ++ 20 est qu'il existe certaines situations dans lesquelles vous devez écrire requires requires. Par exemple, cet exemple de [expr.prim.req] / 3 :

Une expression require peut également être utilisée dans une clause requires ([temp]) comme moyen d'écrire des contraintes ad hoc sur des arguments de modèle tels que celui ci-dessous:

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

Le premier requiert introduit la clause requires , et le second introduit l' expression require .

Quelle est la raison technique derrière le besoin de ce deuxième requiresmot-clé? Pourquoi ne pouvons-nous pas simplement autoriser l'écriture:

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

(Remarque: veuillez ne pas répondre à la grammaire requires)

Barry
la source
25
Suggestion: "Y a-t-il quelque chose qui exige exige exige?". Plus sérieusement, j'ai le sentiment que c'est la même raison derrière noexcept(noexcept(...)).
Quentin
11
Les deux requiressont des homonymes à mon avis: ils se ressemblent, épellent, sentent la même chose, mais sont intrinsèquement différents. Si je devais suggérer une solution, je suggérerais de renommer l'un d'entre eux.
YSC
5
@YSC - co_requires? (Désolé, je n'ai pas pu résister).
StoryTeller - Unslander Monica
122
Où s'arrêtera la folie? La prochaine chose que vous savez, nous l'aurons long long.
Eljay
8
@StoryTeller: "Nécessite nécessite requis?" aurait été encore plus allitératif !!
PW

Réponses:

81

C'est parce que la grammaire l'exige. Cela fait.

Une requirescontrainte n'a pas besoin d' utiliser une requiresexpression. Il peut utiliser n'importe quelle expression constante booléenne plus ou moins arbitraire. Par conséquent, requires (foo)doit être une requirescontrainte légitime .

Une requires expression (cette chose qui teste si certaines choses suivent certaines contraintes) est une construction distincte; il est simplement introduit par le même mot-clé. requires (foo f)serait le début d'une requiresexpression valide .

Ce que vous voulez, c'est que si vous utilisez requiresdans un endroit qui accepte les contraintes, vous devriez être capable de faire une «contrainte + expression» à partir de la requiresclause.

Alors, voici la question: si vous placez requires (foo)dans un endroit qui convient pour une contrainte requiert ... jusqu'où l'analyseur doit-il aller avant de se rendre compte qu'il s'agit d'une contrainte requise plutôt que d'une contrainte + expression comme vous le souhaitez être?

Considère ceci:

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

Si fooest un type, alors (foo)est une liste de paramètres d'une expression requise, et tout ce qui se trouve dans le {}n'est pas le corps de la fonction mais le corps de cette requiresexpression. Sinon, fooest une expression dans une requiresclause.

Eh bien, vous pourriez dire que le compilateur devrait simplement comprendre ce qui fooest en premier. Mais C ++ n'aime vraiment pas quand l'acte de base d'analyse d'une séquence de jetons nécessite que le compilateur comprenne ce que ces identificateurs signifient avant de pouvoir donner un sens aux jetons. Oui, C ++ est sensible au contexte, donc cela se produit. Mais le comité préfère l'éviter dans la mesure du possible.

Alors oui, c'est de la grammaire.

Nicol Bolas
la source
2
Est-il judicieux d'avoir une liste de paramètres avec un type mais sans nom?
NathanOliver
3
@Quentin: Il y a certainement des cas de dépendance au contexte dans la grammaire C ++. Mais le comité essaie vraiment de minimiser cela, et il n'aime certainement pas en ajouter davantage .
Nicol Bolas
3
@RobertAndrzejuk: Si requiresapparaît après un <>ensemble d'arguments de modèle ou après une liste de paramètres de fonction, alors c'est une clause requires. Si requiresapparaît là où une expression est valide, alors il s'agit d'une expression requise. Cela peut être déterminé par la structure de l'arborescence d'analyse, pas par le contenu de l'arborescence d'analyse (les spécificités de la manière dont un identificateur est défini seraient le contenu de l'arbre).
Nicol Bolas
6
@RobertAndrzejuk: Bien sûr, l'expression requiert peut avoir utilisé un mot-clé différent. Mais les mots-clés ont des coûts énormes en C ++, car ils ont le potentiel de casser tout programme utilisant l'identifiant devenu un mot-clé. La proposition de concepts introduisait déjà deux mots clés: conceptet requires. Introduire un troisième, alors que le second serait capable de couvrir les deux cas sans problèmes grammaticaux et peu de problèmes rencontrés par l'utilisateur, est juste un gaspillage. Après tout, le seul problème visuel est que le mot-clé se répète deux fois.
Nicol Bolas
3
@RobertAndrzejuk c'est de toute façon une mauvaise pratique d'inline des contraintes comme ça puisque vous n'obtenez pas de subsomption comme si vous aviez écrit un concept. Donc, prendre un identifiant pour une fonctionnalité non recommandée à une utilisation aussi faible n'est pas une bonne idée.
Rakete1111
60

La situation est exactement analogue à noexcept(noexcept(...)). Bien sûr, cela ressemble plus à une mauvaise chose qu'à une bonne chose, mais laissez-moi vous expliquer. :) Nous allons commencer par ce que vous savez déjà:

C ++ 11 a des " noexcept-clauses" et des "-expressions" noexcept. Ils font des choses différentes.

  • Une noexcept-clause dit: "Cette fonction devrait être no sauf quand ... (une condition)." Il poursuit une déclaration de fonction, prend un paramètre booléen et provoque un changement de comportement dans la fonction déclarée.

  • Une noexcept-expression dit: "Compilateur, veuillez me dire si (une expression) est noexcept." C'est en soi une expression booléenne. Il n'a aucun "effet secondaire" sur le comportement du programme - il demande simplement au compilateur la réponse à une question oui / non. «Est-ce que cette expression est nulle?

Nous pouvons imbriquer une noexcept-expression dans une noexcept-clause, mais nous considérons généralement que c'est un mauvais style de le faire.

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

Il est considéré comme un meilleur style d'encapsuler l' noexceptexpression-dans un trait de type.

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

Le brouillon de travail C ++ 2a contient des « requires-clauses» et des «-expressions» requires. Ils font des choses différentes.

  • Une requires-clause dit: "Cette fonction devrait participer à la résolution des surcharges quand ... (certaines conditions)." Il poursuit une déclaration de fonction, prend un paramètre booléen et provoque un changement de comportement dans la fonction déclarée.

  • Une requiresexpression-indique: "Compilateur, veuillez me dire si (un ensemble d'expressions) est bien formé." C'est en soi une expression booléenne. Il n'a aucun "effet secondaire" sur le comportement du programme - il demande simplement au compilateur la réponse à une question oui / non. "Cette expression est-elle bien formée?"

Nous pouvons imbriquer une requires-expression dans une requires-clause, mais nous considérons généralement que c'est un mauvais style de le faire.

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

Il est considéré comme meilleur style d'encapsuler l' requiresexpression-dans un trait de type ...

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

... ou dans un concept (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
Quuxplusone
la source
1
Je n'accepte pas vraiment cet argument. noexcepta le problème qui noexcept(f())pourrait signifier soit interpréter f()comme un booléen que nous utilisons pour définir la spécification ou vérifier si oui ou non f()est noexcept. requiresn'a pas cette ambiguïté car les expressions dont il vérifie la validité doivent déjà être introduites avec {}s. Après cela, l'argument est essentiellement «la grammaire le dit».
Barry
@Barry: Voir ce commentaire . Il semble qu'ils {}soient facultatifs.
Eric
1
@Eric The {}ne sont pas facultatifs, ce n'est pas ce que montre ce commentaire. Cependant, c'est un excellent commentaire démontrant l'ambiguïté de l'analyse. Accepterait probablement ce commentaire (avec quelques explications) comme une réponse autonome
Barry
1
requires is_nothrow_incrable_v<T>;devrait êtrerequires is_incrable_v<T>;
Ruslan
16

Je pense que la page de concepts de cppreference explique cela. Je peux expliquer avec "maths" pour ainsi dire, pourquoi cela doit être comme ça:

Si vous souhaitez définir un concept, procédez comme suit:

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

Si vous souhaitez déclarer une fonction qui utilise ce concept, procédez comme suit:

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

Maintenant, si vous ne voulez pas définir le concept séparément, je suppose que tout ce que vous avez à faire est une substitution. Prenez cette pièce requires (T x) { x + x; };et remplacez la Addable<T>pièce, et vous obtiendrez:

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

c'est ce que vous demandez.

Le physicien quantique
la source
4
Je ne pense pas que ce soit ce que demande la question. Ceci explique plus ou moins la grammaire.
Passer par
Mais pourquoi ne pouvez-vous pas avoir template<typename T> requires (T x) { x + x; }et exiger que require puisse être à la fois la clause et l'expression?
NathanOliver
2
@NathanOliver: Parce que vous forcez le compilateur à interpréter une construction comme une autre. Une requiresclause -as-constraint n'a pas besoin d' être une requires-expression. C'est simplement une utilisation possible.
Nicol Bolas
2
@TheQuantumPhysicist Ce que je voulais dire avec mon commentaire, c'est que cette réponse explique simplement la syntaxe. Pas quelle vraie raison technique nous devons requires requires. Ils auraient pu ajouter quelque chose à la grammaire pour autoriser template<typename T> requires (T x) { x + x; }mais ils ne l'ont pas fait. Barry veut savoir pourquoi ils ne l'ont pas fait
NathanOliver
3
Si nous jouons vraiment ici à l'ambiguïté de la recherche de la grammaire, d'accord, je vais mordre. godbolt.org/z/i6n8kM template<class T> void f(T) requires requires(T (x)) { (void)x; }; signifie quelque chose de différent si vous supprimez l'un des requireses.
Quuxplusone
12

J'ai trouvé un commentaire d'Andrew Sutton (l'un des auteurs de Concepts, qui l'a implémenté dans gcc) très utile à cet égard, alors j'ai pensé que je le citerais ici dans sa quasi-totalité:

Il n'y a pas si longtemps, les expressions-requises (la phrase introduite par la seconde requiert) n'était pas autorisée dans les expressions-contraintes (la phrase introduite par la première requiert). Il ne pouvait apparaître que dans les définitions de concept. En fait, c'est exactement ce qui est proposé dans la section de ce document où cette allégation apparaît.

Cependant, en 2016, il a été proposé d'assouplir cette restriction [Note de la rédaction: P0266 ]. Notez la suppression du paragraphe 4 dans la section 4 du document. Et donc est né oblige.

Pour dire la vérité, je n'avais jamais mis en œuvre cette restriction dans GCC, donc cela avait toujours été possible. Je pense que Walter a peut-être découvert cela et l'a trouvé utile, ce qui a conduit à cet article.

De peur que quiconque pense que je n'étais pas sensible à l'écriture exige deux fois, j'ai passé un certain temps à essayer de déterminer si cela pouvait être simplifié. Réponse courte: non.

Le problème est qu'il y a deux constructions grammaticales qui doivent être introduites après une liste de paramètres de modèle: très souvent une expression de contrainte (comme P && Q) et parfois des exigences syntaxiques (comme requires (T a) { ... }). C'est ce qu'on appelle une expression d'exigences.

Le premier requiert introduit la contrainte. Le second requiert introduit l'expression require. C'est juste la façon dont la grammaire se compose. Je ne trouve pas du tout cela déroutant.

J'ai essayé, à un moment donné, de les réduire à un seul besoin. Malheureusement, cela conduit à des problèmes d'analyse très difficiles. Vous ne pouvez pas dire facilement, par exemple si un (après le requiert indique une sous-expression imbriquée ou une liste de paramètres. Je ne crois pas qu'il y ait une désambiguïsation parfaite de ces syntaxes (voir la justification de la syntaxe d'initialisation uniforme; ce problème est là aussi).

Vous faites donc un choix: make requiert introduire une expression (comme c'est le cas maintenant) ou lui faire introduire une liste paramétrée d'exigences.

J'ai choisi l'approche actuelle car la plupart du temps (comme presque 100% du temps), je veux autre chose qu'une expression-oblige. Et dans le cas extrêmement rare où je voulais une expression requise pour les contraintes ad hoc, cela ne me dérange vraiment pas d'écrire le mot deux fois. C'est un indicateur évident que je n'ai pas développé une abstraction suffisamment solide pour le modèle. (Parce que si j'avais, il aurait un nom.)

J'aurais pu choisir de faire en sorte que les demandes introduisent une expression requise. C'est en fait pire, car pratiquement toutes vos contraintes commenceraient à ressembler à ceci:

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

Ici, la 2ème demande est appelée une exigence imbriquée; il évalue son expression (l'autre code du bloc de l'expression requires-expression n'est pas évalué). Je pense que c'est bien pire que le statu quo. Maintenant, vous obtenez d'écrire exige deux fois partout.

J'aurais pu aussi utiliser plus de mots-clés. C'est un problème en soi - et ce n'est pas seulement la perte de vélos. Il pourrait y avoir un moyen de "redistribuer" les mots-clés pour éviter la duplication, mais je n'ai pas sérieusement réfléchi. Mais cela ne change pas vraiment l'essence du problème.

Barry
la source
-10

Parce que vous dites qu'une chose A a une exigence B, et l'exigence B a une exigence C.

La chose A nécessite B qui à son tour nécessite C.

La clause «requiert» elle-même requiert quelque chose.

Vous avez la chose A (nécessitant B (nécessitant C)).

Meh. :)

Courses de légèreté en orbite
la source
4
Mais selon les autres réponses, la première et la seconde requiresne sont pas conceptuellement la même chose (l'une est une clause, l'autre une expression). En fait, si je comprends bien, les deux ensembles de ()in requires (requires (T x) { x + x; })ont des significations très différentes (l'extérieur étant facultatif et contenant toujours une constexpr booléenne; l'intérieur étant une partie obligatoire de l'introduction d'une expression requiert et ne permettant pas d' expressions réelles).
Max Langhof
2
@MaxLanghof Êtes-vous en train de dire que les exigences diffèrent? : D
Courses de légèreté en orbite