Quand les parenthèses supplémentaires ont-elles un effet, autre que sur la priorité des opérateurs?

91

Les parenthèses en C ++ sont utilisées à de nombreux endroits: par exemple dans les appels de fonction et les expressions de regroupement pour remplacer la priorité des opérateurs. En dehors des parenthèses supplémentaires illégales (comme autour des listes d'arguments d'appel de fonction), une règle générale - mais pas absolue - de C ++ est que les parenthèses supplémentaires ne font jamais de mal :

5.1 Expressions primaires [expr.prim]

5.1.1 Général [expr.prim.general]

6 Une expression entre parenthèses est une expression principale dont le type et la valeur sont identiques à ceux de l'expression incluse. La présence de parenthèses n'affecte pas si l'expression est une lvalue. L'expression entre parenthèses peut être utilisée exactement dans les mêmes contextes que ceux où l'expression incluse peut être utilisée, et avec la même signification, sauf indication contraire .

Question : dans quels contextes les parenthèses supplémentaires changent-elles la signification d'un programme C ++, autre que la priorité de l'opérateur de base?

REMARQUE : Je considère que la restriction de la syntaxe pointeur vers membre&qualified-id sans parenthèses est en dehors de la portée, car elle restreint la syntaxe plutôt que d'autoriser deux syntaxes avec des significations différentes. De même, l'utilisation de parenthèses dans les définitions de macros de préprocesseur protège également contre la préséance indésirable des opérateurs.

TemplateRex
la source
"Je considère la résolution & (qual-id) vers le pointeur vers membre comme une application de la priorité des opérateurs." -- Pourquoi donc? Si vous omettez les parenthèses entre &(C::f), l'opérande de &est toujours C::f, n'est-ce pas?
@hvd expr.unary.op/4: Un pointeur vers un membre n'est formé que lorsqu'un explicite &est utilisé et que son opérande est un identifiant qualifié non placé entre parenthèses.
TemplateRex
Bon, alors qu'est-ce que cela a à voir avec la priorité des opérateurs? (Nevermind, votre question modifiée clarifie cela.)
@hvd mis à jour, je confondais le RHS avec le LHS dans ce Q&A , et là, les parenthèses sont utilisées pour remplacer la priorité de l'appel de fonction ()sur le sélecteur de pointeur vers membre::*
TemplateRex
1
Je pense que vous devriez être un peu plus précis sur les cas à examiner. Par exemple, les parenthèses autour d'un nom de type pour en faire un opérateur de conversion de style C (quel que soit le contexte) ne font pas du tout une expression entre parenthèses. D'un autre côté, je dirais techniquement que la condition après if ou while est une expression entre parenthèses, mais comme les parenthèses font partie de la syntaxe ici, elles ne devraient pas être prises en compte. L'OMI ne devrait pas non plus l'être, où sans les parenthèses, l'expression ne serait plus analysée comme une seule unité, que la priorité des opérateurs soit impliquée ou non.
Marc van Leeuwen

Réponses:

112

TL; DR

Les parenthèses supplémentaires changent la signification d'un programme C ++ dans les contextes suivants:

  • empêcher la recherche de noms dépendants des arguments
  • activation de l'opérateur virgule dans les contextes de liste
  • résolution d'ambiguïté des analyses vexantes
  • déduire la référence dans les decltypeexpressions
  • prévention des erreurs de macro de préprocesseur

Empêcher la recherche de noms dépendants des arguments

Comme il est détaillé dans l'annexe A de la norme, a post-fix expressiondu formulaire (expression)est un primary expression, mais pas un id-expression, et donc pas un unqualified-id. Cela signifie que la recherche de nom dépendante de l'argument est empêchée dans les appels de fonction de la forme (fun)(arg)par rapport à la forme conventionnelle fun(arg).

3.4.2 Recherche de nom dépendante de l'argument [basic.lookup.argdep]

1 Lorsque l'expression de suffixe dans un appel de fonction (5.2.2) est un identifiant non qualifié , d'autres espaces de noms non pris en compte lors de la recherche non qualifiée habituelle (3.4.1) peuvent être recherchés, et dans ces espaces de noms, une fonction amie de portée d'espace de noms ou des déclarations de modèle de fonction (11.3) non visibles autrement peuvent être trouvées. Ces modifications de la recherche dépendent des types d'arguments (et pour les arguments de modèle de modèle, de l'espace de nom de l'argument de modèle). [ Exemple:

namespace N {
    struct S { };
    void f(S);
}

void g() {
    N::S s;
    f(s);   // OK: calls N::f
    (f)(s); // error: N::f not considered; parentheses
            // prevent argument-dependent lookup
}

—End exemple]

Activation de l'opérateur virgule dans les contextes de liste

L'opérateur virgule a une signification particulière dans la plupart des contextes de type liste (arguments de fonction et de modèle, listes d'initialiseurs, etc.). Les parenthèses du formulaire a, (b, c), ddans de tels contextes peuvent activer l'opérateur virgule par rapport au formulaire normal a, b, c, doù l'opérateur virgule ne s'applique pas.

5.18 Opérateur virgule [expr.comma]

2 Dans les contextes où la virgule a une signification particulière, [Exemple: dans les listes d'arguments de fonctions (5.2.2) et les listes d'initialiseurs (8.5) - exemple de fin] l'opérateur virgule tel que décrit dans l'Article 5 ne peut apparaître qu'entre parenthèses. [ Exemple:

f(a, (t=3, t+2), c);

a trois arguments, dont le second a la valeur 5. —fin exemple]

Résolution de l'ambiguïté des analyses vexantes

La rétrocompatibilité avec C et sa syntaxe de déclaration de fonction arcane peut conduire à des ambiguïtés d'analyse surprenantes, appelées analyses vexantes. Essentiellement, tout ce qui peut être analysé en tant que déclaration sera analysé comme une seule , même si une analyse concurrente s'appliquerait également.

6.8 Résolution d'ambiguïté [stmt.ambig]

1 Il y a une ambiguïté dans la grammaire impliquant des instructions d'expression et des déclarations : Une instruction d'expression avec une conversion de type explicite de style fonction (5.2.3) comme sous-expression la plus à gauche peut être indiscernable d'une déclaration où le premier déclarateur commence par un ( . Dans ces cas , la déclaration est une déclaration .

8.2 Résolution des ambiguïtés [dcl.ambig.res]

1 L'ambiguïté résultant de la similitude entre un cast de style fonction et une déclaration mentionnée au 6.8 peut également se produire dans le contexte d'une déclaration . Dans ce contexte, le choix est entre une déclaration de fonction avec un ensemble redondant de parenthèses autour d'un nom de paramètre et une déclaration d'objet avec un cast de style fonction comme initialiseur. Tout comme pour les ambiguïtés mentionnées en 6.8, la résolution est de considérer toute construction qui pourrait éventuellement être une déclaration comme une déclaration . [Remarque: Une déclaration peut être explicitement désambiguïsée par une conversion de style non fonction, par un = pour indiquer l'initialisation ou en supprimant les parenthèses redondantes autour du nom du paramètre. —End note] [Exemple:

struct S {
    S(int);
};

void foo(double a) {
    S w(int(a));  // function declaration
    S x(int());   // function declaration
    S y((int)a);  // object declaration
    S z = int(a); // object declaration
}

—End exemple]

Un exemple célèbre de ceci est le Most Vexing Parse , un nom popularisé par Scott Meyers dans le point 6 de son livre Effective STL :

ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), // warning! this doesn't do
               istream_iterator<int>());        // what you think it does

Cela déclare une fonction data, dont le type de retour est list<int>. Les données de fonction prennent deux paramètres:

  • Le premier paramètre est nommé dataFile. C'est le type istream_iterator<int>. Les parenthèses autour dataFilesont superflues et ignorées.
  • Le deuxième paramètre n'a pas de nom. Son type est un pointeur vers une fonction ne prenant rien et renvoyant un fichier istream_iterator<int>.

Placer des parenthèses supplémentaires autour du premier argument de fonction (les parenthèses autour du deuxième argument sont illégales) résoudra l'ambiguïté

list<int> data((istream_iterator<int>(dataFile)), // note new parens
                istream_iterator<int>());          // around first argument
                                                  // to list's constructor

C ++ 11 a une syntaxe d'initialisation d'accolades qui permet de contourner ces problèmes d'analyse dans de nombreux contextes.

Déduire le référencé dans les decltypeexpressions

Contrairement à la autodéduction de type, decltypepermet de déduire la référence (références lvalue et rvalue). Les règles distinguent les expressions decltype(e)et decltype((e)):

7.1.6.2 Spécificateurs de type simple [dcl.type.simple]

4 Pour une expression e, le type désigné pardecltype(e) est défini comme suit:

- si eest une expression id sans parenthèse ou un accès de membre de classe non parenthèse (5.2.5), decltype(e)est le type de l'entité nommée par e. S'il n'y a pas une telle entité, ou si enomme un ensemble de fonctions surchargées, le programme est mal formé;

- sinon, si eest une valeur x, decltype(e)est T&&, où Test le type de e;

- sinon, si eest une valeur l, decltype(e)est T&, où Test le type de e;

- sinon, decltype(e)est le type de e.

L'opérande du spécificateur decltype est un opérande non évalué (article 5). [ Exemple:

const int&& foo();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1 = 0;   // type is const int&&
decltype(i) x2;           // type is int
decltype(a->x) x3;        // type is double
decltype((a->x)) x4 = x3; // type is const double&

—Fin exemple] [Remarque: les règles de détermination des types impliquant decltype(auto)sont spécifiées au 7.1.6.4. —End note]

Les règles pour decltype(auto)ont une signification similaire pour les parenthèses supplémentaires dans le RHS de l'expression d'initialisation. Voici un exemple de la FAQ C ++ et de ces questions et réponses connexes

decltype(auto) look_up_a_string_1() { auto str = lookup1(); return str; }  //A
decltype(auto) look_up_a_string_2() { auto str = lookup1(); return(str); } //B

Le premier retourne string, le second retourne string &, qui est une référence à la variable locale str.

Prévention des erreurs liées aux macros de préprocesseur

Il existe une foule de subtilités avec les macros de préprocesseur dans leur interaction avec le langage C ++ proprement dit, dont les plus courantes sont répertoriées ci-dessous

  • en utilisant des parenthèses autour des paramètres de macro à l'intérieur de la définition de macro #define TIMES(A, B) (A) * (B);afin d'éviter la préséance indésirable des opérateurs (par exemple, dans TIMES(1 + 2, 2 + 1)lequel donne 9 mais donnerait 6 sans les parenthèses autour (A)et(B)
  • en utilisant des parenthèses autour des arguments de macro comportant des virgules à l'intérieur: assert((std::is_same<int, int>::value));qui autrement ne compileraient pas
  • en utilisant des parenthèses autour d'une fonction pour protéger contre l'expansion des macros dans les en-têtes inclus: (min)(a, b)(avec l'effet secondaire indésirable de désactiver également ADL)
TemplateRex
la source
7
Ne change pas vraiment la signification du programme, mais la meilleure pratique et affecte les avertissements émis par le compilateur: des parenthèses supplémentaires doivent être utilisées dans if/ whilesi l'expression est une affectation. Par exemple if (a = b)- avertissement (vouliez-vous dire ==?), Tandis que if ((a = b))- aucun avertissement.
Csq
@Csq merci, bonne observation, mais c'est un avertissement par un compilateur particulier et non mandaté par le Standard. Je ne pense pas que cela corresponde à la nature juriste de la langue de ce Q&R.
TemplateRex
Est-ce que (min)(a, b)(avec une mauvaise MACRO min(A, B)) fait partie de la prévention de la recherche de nom dépendante des arguments?
Jarod42
@ Jarod42 Je suppose que oui, mais considérons que ces macros diaboliques et d'autres sortent du cadre de la question :-)
TemplateRex
5
@JamesKanze: Notez que OP et TemplateRex sont la même personne ^ _ ^
Jarod42
4

En général, dans les langages de programmation, les parenthèses "supplémentaires" impliquent qu'elles ne changent pas l'ordre d'analyse syntaxique ou la signification. Ils sont ajoutés pour clarifier l'ordre (priorité des opérateurs) au profit des personnes lisant le code, et leur seul effet serait de ralentir légèrement le processus de compilation et de réduire les erreurs humaines dans la compréhension du code (accélérant probablement le processus de développement global ).

Si un ensemble de parenthèses modifie réellement la façon dont une expression est analysée, elles ne sont par définition pas extra. Les parenthèses qui transforment une analyse illégale / invalide en une analyse légale ne sont pas "extra", bien que cela puisse indiquer une mauvaise conception du langage.

Phil Perry
la source
2
exactement, et c'est aussi la règle générale en C ++ (voir la citation standard dans la question), sauf indication contraire . Souligner ces «faiblesses» était le but de ce Q&R.
TemplateRex