Wann wirken sich zusätzliche Klammern anders als auf die Priorität des Operators aus?

91

Klammern in C ++ werden an vielen Stellen verwendet: z. B. in Funktionsaufrufen und Gruppierungsausdrücken, um die Priorität von Operatoren zu überschreiben. Abgesehen von illegalen zusätzlichen Klammern (wie z. B. Argumentlisten für Funktionsaufrufe) ist eine allgemeine, aber nicht absolute Regel von C ++, dass zusätzliche Klammern niemals schaden :

5.1 Primäre Ausdrücke [expr.prim]

5.1.1 Allgemeines [expr.prim.general]

6 Ein Ausdruck in Klammern ist ein primärer Ausdruck, dessen Typ und Wert mit denen des eingeschlossenen Ausdrucks identisch sind. Das Vorhandensein von Klammern hat keinen Einfluss darauf, ob der Ausdruck ein Wert ist. Der Ausdruck in Klammern kann in genau denselben Kontexten wie diejenigen verwendet werden, in denen der eingeschlossene Ausdruck verwendet werden kann, und mit derselben Bedeutung, sofern nicht anders angegeben .

Frage : In welchen Kontexten ändern zusätzliche Klammern die Bedeutung eines C ++ - Programms, außer dass die Priorität des Operators überschrieben wird?

ANMERKUNG : Ich betrachte die Beschränkung der Zeiger-zu-Element- Syntax auf &qualified-idohne Klammern als außerhalb des Gültigkeitsbereichs liegend, da sie die Syntax einschränkt, anstatt zwei Syntaxen mit unterschiedlichen Bedeutungen zuzulassen. In ähnlicher Weise schützt die Verwendung von Klammern in Präprozessor-Makrodefinitionen auch vor unerwünschter Operatorrangfolge.

TemplateRex
quelle
"Ich betrachte die & (qualifizierte ID) -Auflösung für den Zeiger auf ein Mitglied als eine Anwendung mit Operator-Vorrang." -- Warum ist das so? Wenn Sie die Klammern weglassen &(C::f), ist der Operand von &immer noch C::f, nicht wahr?
@hvd expr.unary.op/4: Ein Zeiger auf ein Mitglied wird nur gebildet, wenn ein expliziter &Operand verwendet wird und sein Operand eine qualifizierte ID ist, die nicht in Klammern steht.
TemplateRex
Richtig, was hat das mit der Priorität des Operators zu tun? (Nevermind, Ihre bearbeitete Frage klärt das auf.)
@hvd aktualisiert, ich habe die RHS mit der LHS in diesen Fragen und Antworten verwechselt , und dort werden die Parens verwendet, um die Priorität des Funktionsaufrufs ()gegenüber dem Zeiger-zu-Mitglied-Selektor::*
TemplateRex
1
Ich denke, Sie sollten etwas genauer sein, welche Fälle berücksichtigt werden müssen. Zum Beispiel machen Klammern um einen Typnamen, um ihn zu einem Cast-Operator im C-Stil zu machen (unabhängig vom Kontext), überhaupt keinen Ausdruck in Klammern. Andererseits würde ich technisch sagen, dass die Bedingung nach if oder while ein Ausdruck in Klammern ist, aber da die Klammern hier Teil der Syntax sind, sollten sie nicht berücksichtigt werden. IMO sollte es auch nicht sein, wenn der Ausdruck ohne Klammern nicht mehr als einzelne Einheit analysiert würde, unabhängig davon, ob es sich um eine Operatorpriorität handelt oder nicht.
Marc van Leeuwen

Antworten:

112

TL; DR

Zusätzliche Klammern ändern die Bedeutung eines C ++ - Programms in den folgenden Kontexten:

  • Verhindern der argumentabhängigen Namenssuche
  • Aktivieren des Kommaoperators in Listenkontexten
  • Mehrdeutigkeitsauflösung von ärgerlichen Parsen
  • Ableiten von Referenz in decltypeAusdrücken
  • Verhindern von Präprozessor-Makrofehlern

Argumentabhängige Namenssuche verhindern

Wie in Anhang A der Norm dargelegt, ist a post-fix expressionder Form (expression)ein primary expression, aber kein id-expressionund daher kein unqualified-id. Dies bedeutet, dass die argumentabhängige Namenssuche bei Funktionsaufrufen des Formulars im (fun)(arg)Vergleich zum herkömmlichen Formular verhindert wird fun(arg).

3.4.2 Argumentabhängige Namenssuche [basic.lookup.argdep]

1 Wenn der Postfix-Ausdruck in einem Funktionsaufruf (5.2.2) eine unqualifizierte ID ist , können andere Namespaces durchsucht werden, die bei der üblichen unqualifizierten Suche (3.4.1) nicht berücksichtigt wurden, und in diesen Namespaces die Namespace-Scope-Friend-Funktion oder Funktionsvorlagenerklärungen (11.3), die ansonsten nicht sichtbar sind, können gefunden werden. Diese Änderungen an der Suche hängen von den Arten der Argumente ab (und bei Vorlagenvorlagenargumenten vom Namespace des Vorlagenarguments). [Beispiel:

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
}

- Beispiel beenden]

Aktivieren des Kommaoperators in Listenkontexten

Der Kommaoperator hat in den meisten listenartigen Kontexten (Funktions- und Vorlagenargumente, Initialisierungslisten usw.) eine besondere Bedeutung. Klammern des Formulars a, (b, c), din solchen Kontexten können den Kommaoperator im Vergleich zum regulären Formular aktivieren, a, b, c, dbei dem der Kommaoperator nicht gilt.

5.18 Kommaoperator [expr.comma]

2 In Kontexten, in denen Komma eine besondere Bedeutung erhält, [Beispiel: In Listen von Argumenten für Funktionen (5.2.2) und Listen von Initialisierern (8.5) - Beispiel) kann der in Abschnitt 5 beschriebene Kommaoperator nur in Klammern stehen. [Beispiel:

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

hat drei Argumente, von denen das zweite den Wert 5 hat. - Beispiel beenden]

Mehrdeutigkeitsauflösung von ärgerlichen Parsen

Die Abwärtskompatibilität mit C und seiner Deklarationssyntax für arkane Funktionen kann zu überraschenden Parsing-Ambiguitäten führen, die als ärgerliche Parses bezeichnet werden. Im Wesentlichen wird alles, was als Deklaration analysiert werden kann, als eine Deklaration analysiert , obwohl auch eine konkurrierende Analyse gelten würde.

6.8 Mehrdeutigkeitsauflösung [stmt.ambig]

1 In der Grammatik gibt es eine Mehrdeutigkeit, die Ausdrucksanweisungen und Deklarationen umfasst : Eine Ausdrucksanweisung mit einer expliziten Typkonvertierung im Funktionsstil (5.2.3) als äußerster äußerer Unterausdruck kann nicht von einer Deklaration unterschieden werden, bei der der erste Deklarator mit einem (beginnt . In diesen Fällen ist die Erklärung eine Erklärung .

8.2 Mehrdeutigkeitsauflösung [dcl.ambig.res]

1 Die Mehrdeutigkeit, die sich aus der Ähnlichkeit zwischen einem Cast im Funktionsstil und einer in 6.8 genannten Deklaration ergibt, kann auch im Zusammenhang mit einer Deklaration auftreten . In diesem Zusammenhang besteht die Wahl zwischen einer Funktionsdeklaration mit einem redundanten Satz von Klammern um einen Parameternamen und einer Objektdeklaration mit einem Cast im Funktionsstil als Initialisierer. Genau wie bei den in 6.8 erwähnten Unklarheiten besteht die Entschließung darin, jedes Konstrukt, das möglicherweise eine Deklaration sein könnte, als Deklaration zu betrachten . [Hinweis: Eine Deklaration kann explizit durch eine Umwandlung ohne Funktionsstil, durch ein = zur Angabe der Initialisierung oder durch Entfernen der redundanten Klammern um den Parameternamen eindeutig definiert werden. —Ende Anmerkung] [Beispiel:

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
}

- Beispiel beenden]

Ein berühmtes Beispiel hierfür ist die Most Vexing Parse , ein Name, der von Scott Meyers in Punkt 6 seines Effective STL- Buches populär gemacht wurde :

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

Dies deklariert eine Funktion data, deren Rückgabetyp ist list<int>. Die Funktionsdaten nehmen zwei Parameter an:

  • Der erste Parameter heißt dataFile. Es ist Typ ist istream_iterator<int>. Die Klammern dataFilesind überflüssig und werden ignoriert.
  • Der zweite Parameter hat keinen Namen. Sein Typ ist ein Zeiger auf eine Funktion, die nichts nimmt und ein zurückgibt istream_iterator<int>.

Durch Platzieren zusätzlicher Klammern um das erste Funktionsargument (Klammern um das zweite Argument sind unzulässig) wird die Mehrdeutigkeit behoben

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

C ++ 11 verfügt über eine Klammer-Initialisierer-Syntax, mit der solche Analyseprobleme in vielen Kontexten umgangen werden können.

Referenz in decltypeAusdrücken ableiten

Im Gegensatz zu autoTyp Abzug decltypeermöglicht referenceness (L - Wert und R - Wert - Referenzen) abgeleitet werden. Die Regeln unterscheiden zwischen decltype(e)und decltype((e))Ausdrücken:

7.1.6.2 Einfache Typspezifizierer [dcl.type.simple]

4 Für einen Ausdruck eist der mit bezeichnete Typdecltype(e) wie folgt definiert:

- Wenn ees sich um einen nicht gekennzeichneten ID-Ausdruck oder einen nicht gekennzeichneten Zugriff auf Klassenmitglieder handelt (5.2.5), decltype(e)ist dies der Typ der Entität, die von benannt wird e. Wenn es keine solche Entität gibt oder wenn eeine Reihe überladener Funktionen benannt wird, ist das Programm fehlerhaft;

- andernfalls, wenn ees sich um einen x-Wert handelt, decltype(e)ist T&&, wo Tist der Typ von e;

- andernfalls, wenn ees sich um einen Wert decltype(e)handelt T&, wo Tist der Typ von e;

- Ansonsten decltype(e)ist die Art von e.

Der Operand des Decltype-Spezifizierers ist ein nicht bewerteter Operand (Abschnitt 5). [Beispiel:

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&

—Ende Beispiel] [Hinweis: Die Regeln zum Bestimmen der beteiligten Typen decltype(auto)sind in 7.1.6.4 angegeben. - Endnote]

Die Regeln für decltype(auto)haben eine ähnliche Bedeutung für zusätzliche Klammern in der rechten Seite des initialisierenden Ausdrucks. Hier ist ein Beispiel aus den C ++ - FAQ und den damit verbundenen Fragen und Antworten

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

Die erste gibt zurück string, die zweite gibt zurück string &, was eine Referenz auf die lokale Variable ist str.

Verhindern von Fehlern im Zusammenhang mit Präprozessormakros

Es gibt eine Vielzahl von Feinheiten mit Präprozessor-Makros in ihrer Interaktion mit der eigentlichen C ++ - Sprache, von denen die häufigsten unten aufgeführt sind

  • Verwenden von Klammern um Makroparameter innerhalb der Makrodefinition #define TIMES(A, B) (A) * (B);, um unerwünschte Operatorprioritäten zu vermeiden (z. B. TIMES(1 + 2, 2 + 1)wenn 9 ergibt, aber 6 ohne die Klammern um (A)und ergibt(B)
  • Verwenden von Klammern um Makroargumente mit Kommas: assert((std::is_same<int, int>::value));die sonst nicht kompiliert würden
  • Verwenden von Klammern um eine Funktion zum Schutz vor Makroerweiterungen in enthaltenen Headern: (min)(a, b)(mit dem unerwünschten Nebeneffekt, dass auch ADL deaktiviert wird)
TemplateRex
quelle
7
Ändert nicht wirklich die Programmbedeutung, sondern die bewährte Methode und wirkt sich auf die vom Compiler ausgegebenen Warnungen aus: Zusätzliche Klammern sollten in if/ verwendet werden, whilewenn der Ausdruck eine Zuweisung ist. ZB if (a = b)- Warnung (meinten Sie ==?), Während if ((a = b))- keine Warnung.
Csq
@Csq danke, gute Beobachtung, aber das ist eine Warnung eines bestimmten Compilers und nicht vom Standard vorgeschrieben. Ich denke nicht, dass dies in die sprachrechtliche Natur dieser Fragen und Antworten passt.
TemplateRex
Ist (min)(a, b)(mit bösem MACRO min(A, B)) Teil der argumentabhängigen Verhinderung der Namenssuche?
Jarod42
@ Jarod42 Ich denke schon, aber lassen Sie uns solche und andere böse Makros als außerhalb des Rahmens der Frage liegend betrachten :-)
TemplateRex
5
@ JamesKanze: Beachten Sie, dass OP und TemplateRex die gleiche Person sind ^ _ ^
Jarod42
4

In Programmiersprachen bedeuten "zusätzliche" Klammern im Allgemeinen, dass sie die syntaktische Analysereihenfolge oder -bedeutung nicht ändern. Sie werden hinzugefügt, um die Reihenfolge (Vorrang des Operators) zu klären, damit die Benutzer den Code lesen können. Ihre einzige Auswirkung besteht darin, den Kompilierungsprozess geringfügig zu verlangsamen und menschliche Fehler beim Verständnis des Codes zu verringern (was wahrscheinlich den gesamten Entwicklungsprozess beschleunigt) ).

Wenn eine Reihe von Klammern tatsächlich die Art und Weise ändert, wie ein Ausdruck analysiert wird, sind sie per Definition nicht extra. Klammern , die ein illegalen / ungültig Parsen in eine juristischen Frage nicht „extra“ drehen, aber das kann ein schlechtes Sprachdesign hinweisen.

Phil Perry
quelle
2
genau, und dies ist auch die allgemeine Regel in C ++ (siehe das Standardzitat in der Frage), sofern nicht anders angegeben . Auf diese "Schwächen" hinzuweisen, war das Ziel dieser Fragen und Antworten.
TemplateRex