【问题标题】:Why do compilers only look backward for type and function declarations?为什么编译器只向后看类型和函数声明?
【发布时间】:2014-04-21 22:28:12
【问题描述】:

这纯粹是为了满足我自己的好奇心,但为什么函数和类型只针对代码中定义的那些较早进行解析,而不是在同一范围内anywhere?

有时会在函数需要相互调用时出现:

void foo(int depth) { bar(depth + 2); }
void bar(int depth) { if (depth > 0) foo(depth - 3); }

您需要将 bar 移到 foo 之前的位置,或者事先声明 bar:

void bar(int);
void foo(int depth) { bar(depth + 2); }
void bar(int depth) { if (depth > 0) foo(depth - 3); }

在更复杂的示例中,您可能需要查看 #include 树和 #ifndef 保护,以找出您的代码无法识别在其他文件中定义的函数或类型的原因,前提是您已经知道应该这样做检查一下。


当然还有这个经典:

typedef struct {
    Node *next;
} Node;

你需要知道这是可能的:

typedef struct Node {
    struct Node *next;
} Node;

...但显然很多人不这样做,所以他们只是到处使用 'void *' 或 'struct Node'。


那么,这些编译器也不能向前解析是有原因的吗?我可以理解预处理器只向后检查,因为事情可以是#undefined,但是一旦声明了一个类型或函数,它就永远存在并且不能被以后的定义重载。

是因为历史原因,最初开发时可用的技术有限吗?还是我遗漏了一些逻辑上的歧义?

【问题讨论】:

  • 我认为更多的是它还没有达到那些功能,而不是它向后阅读。
  • 这里有很多 C-isms 永远不会出现在 C++ 代码中。
  • 不是编译器,而是语言的定义方式。
  • @OliCharlesworth:可以根据编译器工作量来定义语言。强制定义在其使用之前减少编译器所需的传递次数。
  • 在过去 64KB 的内存要花很多钱而且读取穿孔卡片组两次是不切实际的时代,设计语言以便单次编译器可以编译它们是一个强大的目标。 C++ 实际上并非如此,它允许定义使用尚未声明的成员函数的内联函数。 “1.5”通编译器:)

标签: c++ c parsing


【解决方案1】:

您的问题的答案很简单,“如果不这样做,编写编译器会变得更加困难” - 语言规范说它必须是这种方式,但是语言中这种措辞的原因规范是“以这种方式编写编译器更容易”。在某种程度上,也可能是在过去,编译器会“随着时间的推移”生成代码,而不是先读取翻译单元(源文件)中的所有代码,然后再进行处理。

请记住,C 和 C++ 编译器仍然在没有大量内存和非常快的处理器的机器上使用。因此,如果您尝试在小容量机器上编译大量代码,那么“我们不想先阅读所有源代码,然后赞美它”的方法比在 16GB 四核桌面上更有意义机器。我希望您可以一次将一个相当大的项目的整个源代码加载到内存中(例如,LLVM + Clang 中的所有文件大约 450MB,因此可以轻松地放入现代台式机/笔记本电脑的内存中)。

编辑:应该注意“解释型语言”,例如 PHP、JavaScript 和 Basic,通常没有这个要求,但其他编译语言通常有 - 例如 Pascal 有一个特殊的关键字 forward 来告诉这个函数存在的编译器,但我稍后会告诉你它包含什么。

Pascal 和 C(以及 C++,因为它在这方面基于 C)都允许指向尚未完成的结构的指针。只是这个简单的“您还没有所有信息”意味着编译器必须构建类型信息,然后“返回并修复它”[显然仅在需要时]。但它允许我们这样做:

struct X
{
   struct X* next;
   ... 
};

或在 C++ 中:

struct X
{
   X* next; 
   ...
};

Edit2:由 GCC 开发人员 Jan Hubicka 撰写的这篇博客解释了“同时处理所有代码”的一些问题。当然,我们大多数人都不会编译 Firefox 和类似大小的项目,但是大型项目,当您必须一次处理所有代码时,即使在现代机器上,如果开发人员使用现代机器,也会导致“可用内存不足”的问题不要“不时让编译器节食”。

http://hubicka.blogspot.co.uk/2014/04/linktime-optimization-in-gcc-1-brief.html

【讨论】:

  • 您始终可以在类的内联函数定义中引用任何类成员。至少从 C++11 开始,不知道更早。
  • 是的,C++ 11 之前的版本也允许这样做。同样,这意味着编译器必须读取整个类然后处理它,因为编译器在知道前向声明是什么之前不知道如何处理函数。
  • 没有前向声明,这是为程序员键入的东西保留的。就说吧,哪些成员很优秀。
  • 如果我没记错的话,PLL/1 没有这个规则,所以这不仅仅是因为过去的糟糕日子。
  • @Deduplicator:不,我的意思是编译器需要读取所有类,才能找到使用过但尚未定义的内容。我不是说程序员键入前向声明,而是通过使用成员来隐含“声明”。
【解决方案2】:

原因是在需要时出现的实体的所有必要信息允许编译器一次性翻译源文件。参照。 http://blogs.msdn.com/b/ericlippert/archive/2010/02/04/how-many-passes.aspx,用于与 C# 进行比较(不需要先前的声明;但无论如何,一切都在一个类中)。

C 与 Pascal 和其他一些语言共享此功能。编写这样的编译器更容易,而且正如其他人指出的那样,往往会使用更少的内存(但自相矛盾的是,可能会增加编译时间,因为每个翻译单元都会一遍又一遍地解析/编译头文件中的声明)。

【讨论】:

  • 编译器的“硬”部分实际上处于编译的后期阶段,这意味着所有(必需的)代码和数据结构都需要在那里。从源代码或中间形式构建这些文件只占编译时间的一小部分(只要文件不在使用一条湿字符串连接到您的系统的服务器上,但这是基础架构问题,而不是编译器问题 - 缓存本地文件,或者一些类似的,在这种情况下)。
【解决方案3】:

因为任何一种逻辑演绎都需要某种“事物的逻辑顺序”。

你故意忽略事物的顺序,在这里表达你的观点。好的,很好,但这太无知了。

在 C 和 C++ 中,您有一个试图解决一些典型问题的命令:声明和定义。在 C/C++ 中,您可以引用至少已声明的事物,即使它们尚未定义。

但是,这种分离或“声明”和“定义”只是逻辑顺序的放松,归根结底——只是对事物的简化。

想象一个纯粹的简单语言表达序列来描述你的程序(与任何试图表达相同,但甚至试图“编译”成一些实际计算机程序的编程语言相比):A 是 B,但是B 依赖于 C,但 B 与 C 相似,前提是 A 之于 B,D 之于 A,C 之于 A,D 之于 C。

WTF???

如果你能以一种不依赖于任何逻辑顺序的方式为一个专门的现实世界问题推导出一个现实世界的解决方案,那么你就会知道你的答案,而且你可能会变得非常富有,只要知道它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-18
    • 2011-02-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-16
    • 1970-01-01
    相关资源
    最近更新 更多