C++是上下文无关的还是上下文相关的?

443

我经常听到人们声称C++是一种上下文敏感的语言。看看下面这个例子:

a b(c);

这是变量定义还是函数声明?这取决于符号的含义。如果是一个变量,那么a b(c);定义了一个类型为a、名为b且被直接初始化为的变量。但是,如果是一种类型,那么a b(c);声明了一个名为b、接受一个参数 c并返回一个a的函数。

如果您查阅上下文无关语言的定义,您会发现所有语法规则的左侧必须仅由一个非终端符号组成。另一方面,上下文敏感语法允许在左侧使用任意终结符和非终结符的字符串。

在《C++程序设计语言》附录A中浏览时,我找不到一条只有唯一非终端符号以外的任何内容的语法规则。这意味着C++是上下文无关的。(当然,每个上下文无关语言在某种意义上来说也都是上下文敏感的,因为上下文无关语言是上下文敏感语言的子集,但这不是重点。)

那么,C++是上下文无关的还是上下文敏感的?


14
请向我展示一条C++语法规则,其左侧没有单个非终结符号,并且我会立即相信您。 - fredoverflow
10
如果你考虑上下文敏感性的边界线,那么这有点取决于你从哪里划定。我认为有些人争论说几乎所有的静态类型编程语言都是上下文敏感的,不是因为你不能使用 CFG 解析工具构建实用的编译器,而是因为这样的实现“作弊”,会解析一些无效的程序,并在类型检查时才拒绝它们。因此,如果你认为类型不正确的程序不属于该语言(在计算机科学意义上,即一个字符串集合),则应该接受更多的语言而不仅仅是 C++。 - user395760
8
不,你错了。形式语言理论中根本没有“分析”或“语义学”,只有“语言”,它是一组字符串。 - jpalecek
30
到目前为止,没有任何答案实际上涉及到你对“无上下文语法”的定义。我认为,这个问题的正确答案要么引用附录A中不符合你的定义的产生式,要么证明你的定义是错误或不充分的。坚持你的立场! - Lightness Races in Orbit
8
请查看 D 语言的语法真的是上下文无关文法吗?。实际上,我认为每个人都应该阅读该问题及其答案! - Lightness Races in Orbit
显示剩余25条评论
20个回答

384

以下是我(目前)最喜欢的演示,说明为什么解析C++(可能)是图灵完备的,因为它展示了一个程序,只有给定的整数是质数时,该程序在语法上才是正确的。

因此,我断言C++既不是无上下文也不是有上下文敏感的

如果您允许在任何产生式的两侧使用任意符号序列,则会生成乔姆斯基层次结构中的类型0语法(“无限制”),这比上下文敏感语法更强大;无限制语法是图灵完备的。上下文敏感(类型1)语法允许在产生式的左侧使用多个上下文符号,但是相同的上下文必须出现在产生式的右侧(因此称为“上下文敏感”)。[1] 上下文敏感语法等效于线性有界图灵机

在这个示例程序中,素数计算可以由线性有界图灵机执行,因此它并没有完全证明图灵等价性,但重要的是解析器需要执行计算以执行语法分析。它可以是任何可表示为模板实例化的计算,并且有充分的理由相信C++模板实例化是图灵完备的。例如,请参见Todd L. Veldhuizen's 2003 paper
无论如何,C++都可以被计算机解析,因此它肯定可以被图灵机解析。因此,无限制文法可以识别它。实际编写这样的文法是不切实际的,这就是标准没有尝试这样做的原因。(请参见下文。)
“歧义”某些表达的问题大多是一个误导。首先,歧义是特定语法的特征,而不是语言。即使可以证明一种语言没有无歧义的语法,如果它可以被上下文无关文法识别,那么它就是上下文无关的。同样地,如果它不能被上下文无关文法识别,但可以被上下文有关文法识别,那么它就是上下文有关的。歧义是不相关的。
但无论如何,就像下面程序中的第21行(即auto b = foo<IsPrime<234799>>::typen<1>();)一样,这些表达式根本不会产生歧义;它们只是根据上下文以不同的方式解析。在问题的最简表达式中,某些标识符的语法类别取决于它们的声明方式(例如类型和函数),这意味着形式语言必须认识到同一程序中两个任意长度的字符串是相同的(声明和使用)。这可以通过“复制”语法来建模,该语法识别两个连续的完全相同的单词。可以用pumping lemma轻松证明这种语言不是上下文无关的。这种语言的上下文有关语法是可能的,并且在这个问题的答案中提供了一个0型语法:https://math.stackexchange.com/questions/163830/context-sensitive-grammar-for-the-copy-language
如果有人试图编写一个上下文敏感(或无限制)的语法来解析C ++,那么它很可能会用涂鸦填满整个宇宙。编写一个图灵机来解析C ++同样是不可能的任务。即使编写C ++程序也很困难,据我所知,没有一个被证明是正确的。这就是为什么标准不尝试提供完整的形式语法定义,并且选择用技术英语编写一些解析规则的原因。
在C ++标准中看起来像形式语法的东西并不是C ++语言语法的完整形式定义。甚至不是预处理后语言的完整形式定义,这可能更容易形式化。 (然而那不是语言:由标准定义的C ++语言包括预处理器,并且预处理器的操作是通过算法描述的,因为在任何语法形式中描述它都非常困难。在标准的这一部分中描述了词汇分解,包括必须多次应用的规则。)
各种语法(两个重叠的词法分析语法,一个在预处理之前进行,另一个在必要时之后进行,以及“句法”语法)收集在附录A中,并附有重要说明(强调添加):
这份C++语法摘要旨在帮助理解,但它并不是该语言的精确陈述。特别地,此处描述的语法接受有效C++构造的超集。必须应用消歧规则(6.8、7.1、10.2)来区分表达式和声明。此外,必须使用访问控制、歧义和类型规则来淘汰语法上有效但无意义的结构。

最后,这是承诺的程序。如果且仅当IsPrime<N>中的N为质数时,第21行才是语法正确的。否则,typen是一个整数,而不是一个模板,因此typen<1>()被解析为(typen<1)>(),这是语法上不正确的,因为()不是一个语法上有效的表达式。

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] 更准确地说,上下文敏感语法中的每个产生式必须具有以下形式:

αAβ → αγβ

其中A是非终结符,αβ是可能为空的语法符号序列,γ是非空序列。(语法符号可以是终结符或非终结符)。

这可以被理解为只有在上下文[α,β]中才能将A → γ。在上下文无关(第2型)语法中,αβ必须为空。

事实证明,您还可以通过“单调”限制来限制语法,其中每个产生式必须具有以下形式:

α → β,其中|α| ≥ |β| > 0  (|α|表示“α的长度”)

可以证明,由单调文法识别的语言集合与由上下文敏感文法识别的语言集合完全相同。通常情况下,基于单调文法进行证明更容易,因此经常会看到人们将“上下文敏感”用作“单调”的意思。

34
它不仅是上下文敏感的,而且可以根据您可以在模板中表达的任何上下文来进行设置,这些模板是图灵完备的。 - Puppy
7
楼上的@mehrdad,原帖中说的是“上下文敏感的语言”,而不是上下文敏感的语法。歧义是语法的一种特征,而不是语言的特征。这种语言确实是上下文敏感的,但并非因为特定的语法具有歧义。 - rici
4
请注意,我的示例含有歧义。它是一个有效程序的明确表达。如果您更改第21行中的值,则可能会使其构成不合规范的程序。但在任何情况下都不会产生歧义。 - rici
7
我有一个疑问:正如你所展示的,模板评估的结果可以使程序良好形成和不良形成之间产生巨大差异。模板评估是图灵完备的。那么,正确确定一个字符串是否属于C++语言,是否需要图灵完备性?正如你所说,上下文敏感的语言仅仅是“线性有界自动机”,据我所知它不是图灵完备的。或者,你的论点利用了C++标准对某些事物的限制,包括模板评估深度的限制吗? - user395760
6
我的原始版本是这样的(你可以通过在()内放置“0”来实现简单的一个),但我认为这样更有趣,因为它展示了即使要识别一个字符串是否是C ++程序的语法上正确,也需要模板实例化。如果两个分支都编译通过,则我必须更努力地争辩区别是“语义”的论点。有趣的是,尽管我经常被挑战定义“句法”,但没有人提供过“语义”的定义,除了“我认为不是句法的东西”:) - rici
显示剩余50条评论

127

首先,你正确地观察到C++标准的语法结尾没有上下文相关规则,因此该语法是无上下文的。

然而,该语法并不能精确描述C++语言,因为它会产生非C++程序,例如:

int m() { m++; }
或者
typedef static int int;

定义为“合法的 C++ 程序集”的 C++ 语言不是上下文无关的(仅要求变量声明就可能证明它不是上下文无关的)。理论上,您可以在模板中编写图灵完备程序,并根据其结果使程序不合规。

现在,(无知的)人们(通常不是语言理论家,而是解析器设计师)通常在以下一些含义上使用“非上下文无关”

  • 有歧义
  • 不能用 Bison 解析
  • 不是 LL(k)、LR(k)、LALR(k) 或他们选择的任何解析器定义的语言类

标准后面的语法不满足这些类别(即存在歧义,不是 LL(k)...),因此对于它们来说,C++ 语法“不是上下文无关的”。从某种意义上说,他们是正确的,因为很难生成可工作的 C++ 解析器。

请注意,这里使用的属性与上下文无关语言只有弱连接 - 歧义与上下文敏感没有任何关系(实际上,上下文敏感规则通常有助于消除产生式的歧义),其他两者仅仅是上下文无关语言的子集。并且解析上下文无关语言不是线性过程(尽管解析确定性上下文无关语言是线性的)。


8
歧义与上下文敏感性无关。这也是我的直觉,所以我很高兴看到有人(a)同意,并且(b)在我不能解释的情况下进行了解释。我相信这排除了所有基于 a b(c); 的论点,并且部分满足了最初的问题前提,即“经常听到”的主张上下文敏感性是由于歧义......特别是对于 语法 ,即使在MVP中也实际上没有歧义。 - Lightness Races in Orbit
7
标准规定:“有一种实现定义的量,它指定了递归实例化的总深度上限,可能涉及多个模板。在实例化中无限递归的结果是未定义的。”(14.7.1p15)我理解这意味着实现不需要理解每个有效的C++程序,但过深的递归深度并不代表程序无效。只有那些具有无限递归深度的程序才被标记为无效。 - rici
3
@KonradRudolph表示,他不同意这是“常用参考资料”。我读了那篇相当复杂的文章,但仍无法充分理解并找出这个小事实,这足以证明它并不普遍易懂。这并不像你说的“计算机通常使用电力”或“比特可以是真或假”之类的简单知识。 - Lightness Races in Orbit
3
如果这个事实真的是如此广为人知,那么我认为找到相关引用材料会比长时间地争论是否提供引用材料更容易得多。更不用说建设性的争论了。 - Samuel Edwin Ward
5
据我所知,@Konrad 在说“上下文敏感等价于图灵完备”时是错误的(至少如果他用“图灵完备”表示“可递归枚举”)。而且他一直无法认识到这个错误。这里有一个参考链接,可以了解所涉及的正确的集合包含关系:http://en.wikipedia.org/wiki/Chomsky_hierarchy。 - pnkfelix
显示剩余14条评论

62

是的。以下表达式在不同的类型解析上下文中具有不同的操作顺序

编辑:当实际操作顺序变化时,使用“常规”编译器在装饰AST之前解析它(传播类型信息)变得非常困难。与此相比,其他上下文敏感的事情“相当容易”,但这并不意味着模板评估很容易。

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

跟随着:

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

为什么不能像C语言那样通过记住哪些类型定义在作用域内来解决这个问题? - Blaisorblade
1
@Blaisorblade:使编译器“干净”的一种方法是将任务分成独立的步骤链,例如从输入创建解析树,然后进行类型分析。C++ 强制你要么 1) 将这些步骤合并为一个步骤,要么 2) 根据所有可能的解释解析文档,并允许类型解析阶段将其缩小到正确的解释。 - Sam Harwell
@280Z28:同意,但这也适用于C语言;我认为对这个问题的一个好回答应该展示为什么C++比C更糟糕。这里链接的博士论文做到了这一点: https://dev59.com/6nVC5IYBdhLWcg3wnCSA#243447 - Blaisorblade

29
回答你的问题,你需要区分两个不同的问题。
  1. 几乎所有编程语言的语法都是无上下文的。通常,它以扩展的Backus-Naur形式或无上下文语法的形式给出。

  2. 然而,即使程序符合编程语言定义的无上下文语法,它也不一定是一个有效的程序。有许多非无上下文属性,程序必须满足才能成为有效的程序。例如,最简单的这种属性是变量的作用域。

总之,C++是否是无上下文取决于你提出的问题。


7
有趣的是,你通常需要将“仅仅是语法”级别降低到你预期的水平以下,以便为你的编程语言获得一个CFG。以C语言为例。你可能认为C语言中一个简单变量声明的语法规则是VARDECL: TYPENAME IDENTIFIER,但实际上你不能这样做,因为在CF(上下文无关)的级别上你不能区分类型名称和其他标识符。另一个例子:在CF级别上,你无法确定是否将a*b解析为变量声明(指向a的指针类型的b)还是乘法运算。 - LaC
2
@LaC:是的,谢谢你指出这个问题!顺便说一下,我相信「mere syntax」应该有一个更常用的技术术语。有人知道正确的术语吗? - Dan
5
你说的是某些无上下文语法提供的语言近似。当然,根据定义,这种近似是无上下文的。这就是在讨论编程语言时经常使用“语法”的意义。 - reinierpost

15
你可能想看一下Bjarne Stroustrup的C++设计与演化。在这本书中,他描述了他尝试使用yacc(或类似工具)来解析早期版本的C ++时遇到的问题,并希望他当时使用递归下降分析法。

哇...谢谢。我想知道是否真的有必要考虑使用比 CFG 更强大的工具来解析任何人造语言。 - Dervin Thunk
这是一本了解C++的原因的好书。我推荐这本书和Lippman的《深入理解C++对象模型》,以便了解C++的工作原理。虽然两本书都有点过时,但仍然值得一读。 - Matt Price
"Meta-S" 是Quinn Tyler Jackson开发的一种上下文敏感的解析引擎。虽然我没有使用过它,但他讲述了一个令人印象深刻的故事。请查看他在comp.compilers中的评论,并访问http://www.rnaparse.com/MetaS%20defined.htm。 - Ira Baxter
@Ira:底部有一个被删除的答案,你应该能够从Quinn Tyler Jackson那里读到关于Meta-S的内容。它(Meta-S)现在可能已经消失了。 - Jonathan Leffler
@Jonathan:啊,是的,这是典型的Quinn回答。祝你好运。总有一天我应该去读一下他写的技术报告(我想是技术报告吧)。我理解它为什么(可能)已经死了;让人们接受一个晦涩的技术是非常困难的。正如我所做的事情又是另一个例子 :-{ - Ira Baxter
显示剩余3条评论

13

是的,C++ 是上下文敏感的,非常上下文敏感。不能使用上下文无关的解析器仅通过解析文件来构建语法树,因为在某些情况下,您需要知道以前的符号才能决定(即在解析时构建符号表)。

第一个例子:

A*B;

这是一个乘法表达式吗?

还是这是声明变量 B 为类型为A的指针吗?

如果 A 是一个变量,那么它是一个表达式,如果 A 是类型,则是指针声明。

第二个例子:

A B(bar);
这是一个以bar类型作为参数的函数原型吗?
或者
这是声明一个类型为A的变量B,并使用bar常量作为初始化器调用了A的构造函数吗?
你需要再次从符号表中确认bar是一个变量还是一个类型。
第三个例子:
class Foo
{
public:
    void fn(){x*y;}
    int x, y;
};

在解析时构建符号表无法帮助的情况是,x和y的声明出现在函数定义之后。因此,您需要先扫描类定义,然后在第二次遍历中查看方法定义,以确定x*y是一个表达式,而不是指针声明或其他什么。


1
A B();即使在函数定义中也是一个函数声明。寻找最令人烦恼的解析... - AProgrammer
你不能仅通过解析文件来构建语法树。这是错误的。请看我的回答。 - Ira Baxter

12

我感觉在“上下文相关”这个术语的正式定义和非正式用法之间存在一些混淆。前者有一个明确定义的含义,而后者用于表示“需要上下文才能解析输入”的意思。

这个问题也在这里被问到了:Context-sensitivity vs Ambiguity

这是一个无上下文文法:

<a> ::= <b> | <c>
<b> ::= "x"
<c> ::= "x"

这是一个不确定的语法,因此为了解析输入的"x",你需要一些上下文(或者容忍不确定性,或者发出警告:"警告:E8271 - 第115行的输入存在歧义")。但它肯定不是一种上下文敏感的语法。


左侧产生式上有多个符号是如何解决这个问题的?我认为这个答案并没有回答这个问题。 - user541686
1
我的回答是针对第一句话:“我经常听到有人声称C++是一种上下文敏感的语言。” 如果这些声明在非正式场合下使用“上下文敏感”这个表达,那么就没有问题。我认为C++并不是正式的上下文敏感语言。 - Omri Barel
我认为C++ 形式上具有上下文敏感性,但我遇到的问题是我不明白一个上下文敏感文法如何在解析C++方面比CFG更成功。 - user541686

10

C++使用GLR解析器进行解析,这意味着在解析源代码时,解析器可能会遇到歧义,但它应该继续并决定稍后使用哪个语法规则。

另外,请参阅:

为什么不能使用LR(1)解析器解析C ++?


请记住,上下文无关语法无法描述编程语言语法的所有规则。例如,属性语法用于检查表达式类型的有效性。

int x;
x = 9 + 1.0;

你无法使用上下文无关文法来描述以下规则: 赋值号右侧应该与左侧的类型相同。


4
大多数 C++ 解析器不使用 GLR 分析技术,GCC 也不使用。但有一些解析器使用 GLR 技术,您可以查看 http://www.semanticdesigns.com/Products/FrontEnds/CppFrontEnd.html 了解一个使用该技术的解析器。 - Ira Baxter

6

没有一种类似Algol的语言是上下文无关的,因为它们有规则来限制表达式和语句中标识符出现的类型,并且在声明和使用之间可以出现无限数量的语句。

通常的解决方案是编写一个上下文无关的解析器,实际上接受有效程序的超集,并将上下文敏感部分放入与规则相关的“语义”代码中。

C++通过其图灵完备的模板系统远远超越了这个范围。请参见Stack Overflow Question 794015


5
C++标准中的产物都是以无上下文语法书写的,但我们都知道这并不能准确地定义语言。目前语言中大多数人所看到的模糊之处可以通过上下文有关语法来消除歧义(我相信)。
最明显的例子是考虑"最令人困惑的解析":int f(X);。如果X是一个值,那么它定义了f作为一个变量,并将其初始化为X。如果X是一种类型,则它定义了f作为一个函数,该函数接受一个类型为X的参数。
从语法的角度来看,我们可以这样看待它:
A variable_decl ::= <type> <identifier> '(' initializer ')' ';'

B function_decl ::= <type> <identifier> '(' param_decl ')' ';'

A ::= [declaration of X as value]
B ::= [declaration of X as type]

当然,为了完全正确,我们需要添加一些额外的“内容”,以考虑其他类型的声明可能介入的可能性(即,A和B应该都是“包括对X声明的声明...”或类似这样的东西)。
尽管如此,这仍然与典型的CSG有所不同(或者至少我记得的那样)。这取决于构建符号表——特别是识别X作为类型或值的部分,而不仅仅是在此之前的某种语句类型,而是正确的符号/标识符的正确语句类型。
因此,我需要进行一些查找才能确定,但我的直觉是,这并不真正符合CSG的要求,至少不是通常使用的术语。

(无上下文)产生式定义了最令人困扰的解析,使其可以被上下文无关的解析引擎解析。这将决定哪些多重解释是有效的问题推迟到解析完成之后,但这只是使解析器和名称解析器的工程更容易,因为它们是模块化的,而不像传统的C++解析器那样纠缠不清。请参阅AST以获取最令人困扰的解析:https://dev59.com/omQm5IYBdhLWcg3wug4u#17393852 - Ira Baxter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接