web-dev-qa-db-fra.com

Pourquoi avons-nous besoin d'exige?

L'un des coins 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]/ :

Une require-expression peut également être utilisée dans une require-clause ( [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 d'exigence , et le second introduit l'expression require-expression .

Quelle est la raison technique derrière le besoin de ce deuxième mot clé requires? 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 que la grammaire requires it)

158
Barry

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

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

Un requiresexpression (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 expression requires valide.

Ce que vous voulez, c'est que si vous utilisez requires dans un endroit qui accepte les contraintes, vous devriez pouvoir créer une "contrainte + expression" à partir de la clause requires.

Voici donc la question: si vous placez requires (foo) dans un endroit qui convient à une contrainte requiert ... jusqu'où l'analyseur doit-il aller avant qu'il puisse se rendre compte qu'il s'agit d'un requiert contrainte plutôt qu'une contrainte + expression comme vous le souhaitez?

Considère ceci:

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

Si foo est un type, alors (foo) est une liste de paramètres d'une expression requise, et tout dans le {} n'est pas le corps de la fonction mais le corps de cette expression requires. Sinon, foo est une expression dans une clause requires.

Eh bien, vous pourriez dire que le compilateur devrait juste comprendre ce qu'est foo en premier. Mais C++ vraiment ne l'aime pas lorsque l'acte de base d'analyser une séquence de jetons nécessite que le compilateur comprenne ce que ces identificateurs signifient avant de pouvoir comprendre les jetons. Oui, C++ est sensible au contexte, donc cela se produit. Mais le comité préfère l'éviter autant que possible.

Alors oui, c'est de la grammaire.

78
Nicol Bolas

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. :) Commençons par ce que vous savez déjà:

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

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

  • Une expression noexcept- dit: "Compilateur, veuillez me dire si (une expression) n'est pas sauf." 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 n'est pas sauf?"

Nous pouvons imbriquer une expression noexcept- dans une clause noexcept-, 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 pour encapsuler l'expression noexcept- 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 comporte des "requires- clauses" et "requires- expressions". Ils font des choses différentes.

  • Une clause requires- dit: "Cette fonction devrait participer à la résolution de surcharge lorsque ... (une condition)." Il va sur une déclaration de fonction, prend un paramètre booléen et provoque un changement de comportement dans la fonction déclarée.

  • Une expression requires- dit: "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 expression requires- dans une clause requires-, 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 un meilleur style pour encapsuler l'expression requires- 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
59
Quuxplusone

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

Si vous souhaitez définir un concept, vous 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, vous 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 partie requires (T x) { x + x; }; et remplacez la partie Addable<T>, 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.

15

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

Il n'y a pas si longtemps, les expressions-requis (la phrase introduite par le second requiert) n'étaient pas autorisées dans les expressions-contraintes (la phrase introduite par le premier 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 le biffage du paragraphe 4 dans la section 4 du document. Et ainsi est né nécessite des exigences.

À vrai dire, je n'avais jamais réellement 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, menant à ce document.

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

Le problème est qu'il existe 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 requise.

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

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 facilement dire, 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 d'une syntaxe d'initialisation uniforme; ce problème est là aussi).

Vous faites donc un choix: make nécessite d'introduire une expression (comme il le fait maintenant) ou de lui faire introduire une liste paramétrée d'exigences.

J'ai choisi l'approche actuelle car la plupart du temps (comme dans presque 100% des cas), je veux autre chose qu'une expression-obligée. 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é d'abstraction suffisamment saine pour le modèle. (Parce que si je l'avais, il aurait un nom.)

J'aurais pu choisir de faire en sorte que le require introduise une require-expression. 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, le 2e requiert est appelé une exigence imbriquée; il évalue son expression (tout autre code dans le bloc de la require-expression n'est pas évalué). Je pense que c'est bien pire que le statu quo. Maintenant, vous pouvez écrire deux fois partout.

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

10
Barry