443 Stimmen

Ist C++ kontextfrei oder kontextabhängig?

Ich höre oft, dass C++ eine kontextabhängige Sprache ist. Nehmen Sie das folgende Beispiel:

a b(c);

Ist dies eine Variablendefinition oder eine Funktionsdeklaration? Das hängt von der Bedeutung des Symbols c . Si c ist eine variabel entonces a b(c); definiert eine Variable namens b vom Typ a . Sie wird direkt initialisiert mit c . Aber wenn c ist eine Typ entonces a b(c); deklariert eine Funktion namens b die eine c und gibt eine a .

Wenn Sie die Definition von kontextfreien Sprachen nachschlagen, werden Sie feststellen, dass alle Grammatikregeln linke Seiten haben müssen, die aus genau einem nicht-terminalen Symbol bestehen. In kontextsensitiven Grammatiken hingegen sind auf der linken Seite beliebige Zeichenketten aus terminalen und nichtterminalen Symbolen zulässig.

Beim Durchblättern des Anhangs A von "The C++ Programming Language" konnte ich keine einzige Grammatikregel finden, die auf ihrer linken Seite etwas anderes als ein einzelnes nichtterminales Symbol enthielt. Das würde bedeuten, dass C++ kontextfrei ist. (Natürlich ist jede kontextfreie Sprache auch kontextsensitiv in dem Sinne, dass die kontextfreien Sprachen eine Teilmenge der kontextsensitiven Sprachen bilden, aber das ist nicht der Punkt.)

Ist C++ also kontextfrei oder kontextsensitiv?

391voto

rici Punkte 218780

Nachstehend meine (derzeitige) Lieblingsdemonstration, warum das Parsen von C++ (wahrscheinlich) Turing-komplett da es ein Programm zeigt, das syntaktisch korrekt ist, wenn und nur wenn eine gegebene ganze Zahl prim ist.

Ich behaupte also, dass C++ ist weder kontextfrei noch kontextsensitiv .

Wenn Sie beliebige Symbolsequenzen auf beiden Seiten einer Produktion zulassen, erzeugen Sie eine Typ-0-Grammatik ("unrestricted") in der Chomsky-Hierarchie die leistungsfähiger ist als eine kontextabhängige Grammatik; unbeschränkte Grammatiken sind Turing-komplett. Eine kontextsensitive (Typ-1) Grammatik erlaubt mehrere Kontextsymbole auf der linken Seite einer Produktion, aber derselbe Kontext muss auf der rechten Seite der Produktion erscheinen (daher der Name "kontextsensitiv"). [1] Kontextsensitive Grammatiken sind äquivalent zu linear begrenzte Turing-Maschinen .

Im Beispielprogramm könnte die Primzahlberechnung von einer linear begrenzten Turing-Maschine durchgeführt werden, so dass die Turing-Äquivalenz nicht ganz bewiesen ist, aber der wichtige Teil ist, dass der Parser die Berechnung durchführen muss, um die syntaktische Analyse durchzuführen. Es könnte jede beliebige Berechnung sein, die als Schabloneninstanziierung ausgedrückt werden kann, und es gibt allen Grund zu der Annahme, dass die C++-Schabloneninstanziierung Turing-komplett ist. Siehe zum Beispiel, Todd L. Veldhuizens Arbeit von 2003 .

Unabhängig davon kann C++ von einem Computer geparst werden, so dass es sicherlich auch von einer Turing-Maschine geparst werden könnte. Folglich könnte eine unbeschränkte Grammatik es erkennen. Eine solche Grammatik zu schreiben, wäre jedoch unpraktisch, weshalb der Standard dies auch nicht versucht. (Siehe unten.)

Das Problem der "Mehrdeutigkeit" bestimmter Ausdrücke ist meist ein Ablenkungsmanöver. Zunächst einmal ist Mehrdeutigkeit ein Merkmal einer bestimmten Grammatik, nicht einer Sprache. Selbst wenn eine Sprache nachweislich keine eindeutigen Grammatiken hat, ist sie kontextfrei, wenn sie von einer kontextfreien Grammatik erkannt werden kann. Ebenso ist sie kontextsensitiv, wenn sie nicht von einer kontextfreien Grammatik erkannt werden kann, aber von einer kontextsensitiven Grammatik erkannt werden kann. Mehrdeutigkeit ist nicht relevant.

Aber in jedem Fall, wie Zeile 21 (d.h. auto b = foo<IsPrime<234799>>::typen<1>(); ) im folgenden Programm sind die Ausdrücke überhaupt nicht mehrdeutig; sie werden einfach je nach Kontext unterschiedlich geparst. In der einfachsten Ausprägung des Problems hängt die syntaktische Kategorie bestimmter Bezeichner davon ab, wie sie deklariert wurden (z. B. Typen und Funktionen), was bedeutet, dass die formale Sprache die Tatsache erkennen müsste, dass zwei Zeichenketten beliebiger Länge im selben Programm identisch sind (Deklaration und Verwendung). Dies kann durch die "Kopier"-Grammatik modelliert werden, d.h. die Grammatik, die zwei aufeinanderfolgende exakte Kopien desselben Wortes erkennt. Es ist leicht zu beweisen mit der Schleifensatz dass diese Sprache nicht kontextfrei ist. Eine kontextsensitive Grammatik für diese Sprache ist möglich, und eine Grammatik vom Typ 0 ist in der Antwort auf diese Frage enthalten: https://math.stackexchange.com/questions/163830/context-sensitive-grammar-for-the-copy-language .

Würde man versuchen, eine kontextabhängige (oder uneingeschränkte) Grammatik für die Analyse von C++ zu schreiben, würde das Universum wahrscheinlich mit Kritzeleien gefüllt werden. Eine Turing-Maschine zu schreiben, um C++ zu parsen, wäre ein ebenso unmögliches Unterfangen. Selbst das Schreiben eines C++-Programms ist schwierig, und soweit ich weiß, wurde noch kein einziges Programm als korrekt erwiesen. Aus diesem Grund versucht die Norm nicht, eine vollständige formale Grammatik bereitzustellen, und entscheidet sich dafür, einige der Parsing-Regeln in technischem Englisch zu verfassen.

Was in der C++-Norm wie eine formale Grammatik aussieht, ist nicht die vollständige formale Definition der Syntax der Sprache C++. Es ist nicht einmal die vollständige formale Definition der Sprache nach der Vorverarbeitung, die einfacher zu formalisieren sein könnte. (Das wäre allerdings nicht die Sprache: Die C++-Sprache, wie sie in der Norm definiert ist, enthält den Präprozessor, und die Funktionsweise des Präprozessors wird algorithmisch beschrieben, da sie in einem grammatikalischen Formalismus nur sehr schwer zu beschreiben wäre. In diesem Abschnitt der Norm wird die lexikalische Dekomposition beschrieben, einschließlich der Regeln, bei denen sie mehr als einmal angewendet werden muss).

Die verschiedenen Grammatiken (zwei sich überschneidende Grammatiken für die lexikalische Analyse, von denen die eine vor der Vorverarbeitung und die andere, falls erforderlich, danach stattfindet, sowie die "syntaktische" Grammatik) sind in Anhang A zusammengestellt, mit dem folgenden wichtigen Hinweis (Hervorhebung hinzugefügt):

Diese Zusammenfassung der C++-Syntax ist als Verständnishilfe gedacht. Es handelt sich nicht um eine genaue Wiedergabe der Sprache . Insbesondere akzeptiert die hier beschriebene Grammatik eine Obermenge der gültigen C++-Konstrukte . Disambiguierungsregeln (6.8, 7.1, 10.2) müssen angewendet werden, um Ausdrücke von Deklarationen zu unterscheiden. Außerdem müssen Zugriffskontroll-, Mehrdeutigkeits- und Typregeln angewandt werden, um syntaktisch gültige, aber sinnlose Konstrukte auszusortieren.

Endlich, hier ist das versprochene Programm. Zeile 21 ist syntaktisch korrekt, wenn und nur wenn das N in IsPrime<N> ist primär. Andernfalls, typen ist eine Ganzzahl, keine Vorlage, also typen<1>() wird geparst als (typen<1)>() was syntaktisch falsch ist, weil () ist kein syntaktisch gültiger Ausdruck.

template<bool V> struct answer { answer(int) {} bool operator()(){return V;}};

template<bool no, bool yes, int f, int p> struct IsPrimeHelper
  : IsPrimeHelper<p % f == 0, f * f >= p, f + 2, p> {};
template<bool yes, int f, int p> struct IsPrimeHelper<true, yes, f, p> { using type = answer<false>; };
template<int f, int p> struct IsPrimeHelper<false, true, f, p> { using type = answer<true>; };

template<int I> using IsPrime = typename IsPrimeHelper<!(I&1), false, 3, I>::type;
template<int I>
struct X { static const int i = I; int a[i]; }; 

template<typename A> struct foo;
template<>struct foo<answer<true>>{
  template<int I> using typen = X<I>;
};
template<> struct foo<answer<false>>{
  static const int typen = 0;
};

int main() {
  auto b = foo<IsPrime<234799>>::typen<1>(); // Syntax error if not prime
  return 0;
}

[1] Um es technisch auszudrücken, muss jede Produktion in einer kontextsensitiven Grammatik von der Form sein:

A

何所 A ist ein Nicht-Terminal und , sind möglicherweise leere Folgen von Grammatiksymbolen, und ist eine nicht leere Folge. (Grammatiksymbole können entweder Terminale oder Nicht-Terminale sein).

Dies kann wie folgt gelesen werden A nur im Zusammenhang mit [, ] . In einer kontextfreien Grammatik (Typ 2), y muss leer sein.

Es stellt sich heraus, dass man Grammatiken auch mit der "monotonen" Restriktion einschränken kann, bei der jede Produktion von der Form sein muss:

donde || || > 0 ( || bedeutet "die Länge der ")

Es ist möglich zu beweisen, dass die Menge der Sprachen, die von monotonen Grammatiken erkannt werden, genau dieselbe ist wie die Menge der Sprachen, die von kontextsensitiven Grammatiken erkannt werden, und es ist oft der Fall, dass es einfacher ist, Beweise auf monotone Grammatiken zu stützen. Daher wird "kontextsensitiv" häufig so verwendet, als ob es "monoton" bedeuten würde.

128voto

jpalecek Punkte 45829

Erstens haben Sie zu Recht festgestellt, dass es in der Grammatik am Ende des C++-Standards keine kontextsensitiven Regeln gibt, so dass die Grammatik ist kontextfrei.

Diese Grammatik beschreibt jedoch nicht genau die Sprache C++, denn sie erzeugt Nicht-C++-Programme wie

int m() { m++; }

o

typedef static int int;

Die C++-Sprache, definiert als "die Menge wohlgeformter C++-Programme", ist nicht kontextfrei (es lässt sich zeigen, dass die bloße Forderung nach der Deklaration von Variablen dies bewirkt). Angesichts der Tatsache, dass man theoretisch Turing-komplette Programme in Schablonen schreiben kann und ein Programm auf der Grundlage ihres Ergebnisses zu einem schlecht geformten Programm machen kann, ist sie nicht einmal kontextsensitiv.

Nun verwenden (unwissende) Menschen (in der Regel keine Sprachtheoretiker, sondern Parserentwickler) den Begriff "nicht kontextfrei" typischerweise in einer der folgenden Bedeutungen

  • zweideutig
  • kann nicht mit Bison geparst werden
  • nicht LL(k), LR(k), LALR(k) oder welche vom Parser definierte Sprachklasse sie auch immer gewählt haben

Die Grammatik auf der Rückseite des Standards erfüllt diese Kategorien nicht (d.h. sie ist mehrdeutig, nicht LL(k)...), also ist die C++-Grammatik für sie "nicht kontextfrei". Und in gewisser Weise haben sie Recht, dass es verdammt schwer ist, einen funktionierenden C++-Parser zu entwickeln.

Beachten Sie, dass die hier verwendeten Eigenschaften nur schwach mit kontextfreien Sprachen verbunden sind - Mehrdeutigkeit hat nichts mit Kontextsensitivität zu tun (tatsächlich helfen kontextsensitive Regeln typischerweise bei der Disambiguierung von Produktionen), die anderen beiden sind lediglich Teilmengen von kontextfreien Sprachen. Und das Parsen kontextfreier Sprachen ist kein linearer Prozess (das Parsen deterministischer Sprachen hingegen schon).

62voto

Sam Harwell Punkte 94511

Ja. Der folgende Ausdruck hat eine andere Reihenfolge der Operationen abhängig von typaufgelöster Kontext :

Bearbeiten: Wenn die tatsächliche Reihenfolge der Operation variiert, macht es unglaublich schwierig, einen "normalen" Compiler zu verwenden, der einen undekorierten AST parst, bevor er ihn dekoriert (Typinformationen propagiert). Andere erwähnte kontextsensitive Dinge sind im Vergleich dazu "ziemlich einfach" (nicht, dass die Auswertung von Vorlagen überhaupt einfach wäre).

#if FIRST_MEANING
   template<bool B>
   class foo
   { };
#else
   static const int foo = 0;
   static const int bar = 15;
#endif

Gefolgt von:

static int foobar( foo < 2 ? 1 < 1 : 0 > & bar );

30voto

Dan Punkte 1529

Um Ihre Frage zu beantworten, müssen Sie zwischen zwei verschiedenen Fragen unterscheiden.

  1. Die reine Syntax fast aller Programmiersprachen ist kontextfrei. In der Regel wird sie in Form einer erweiterten Backus-Naur-Form oder einer kontextfreien Grammatik angegeben.

  2. Doch selbst wenn ein Programm der kontextfreien Grammatik der Programmiersprache entspricht, ist es nicht unbedingt ein gültig Programm. Es gibt viele nicht-kontextfreie Eigenschaften, die ein Programm erfüllen muss, um ein gültiges Programm zu sein. Die einfachste dieser Eigenschaften ist z. B. der Geltungsbereich von Variablen.

Ob C++ kontextfrei ist oder nicht, hängt also von der Frage ab, die Sie stellen.

15voto

Sie sollten einen Blick werfen auf Der Entwurf und die Entwicklung von C++ von Bjarne Stroustrup. Darin beschreibt er seine Probleme beim Versuch, eine frühe Version von C++ mit yacc (oder ähnlichem) zu parsen, und wünscht sich, er hätte stattdessen rekursiven Abstieg verwendet.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X