我在想为什么要有stdlib之类的标准库(在我学过的所有编程语言中,例如C ++,Java,Python),而不要使用类似的“函数”作为语言本身的原始形式。
我在想为什么要有stdlib之类的标准库(在我学过的所有编程语言中,例如C ++,Java,Python),而不要使用类似的“函数”作为语言本身的原始形式。
Answers:
请允许我扩展一下@Vincent(+1)的好答案:
为什么编译器不能简单地将函数调用转换为一组指令?
它可以并且至少通过两种机制来做到这一点:
内联函数调用 -在翻译过程中,编译器可以直接内联其实现替换源代码调用,而不用实际调用该函数。函数仍然需要在某个地方定义一个实现,并且可以在标准库中实现。
内在函数 —内在函数是已通知编译器的函数,而不必在库中找到该函数。这些通常是为那些实际上无法以其他任何方式访问的硬件功能保留的,它们是如此简单,以至于甚至汇编语言库函数调用的开销也被认为很高。(编译器通常只能自动以其语言内联源代码,而不能自动嵌入内联机制所在的汇编函数。)
尽管如此,最好的选择有时还是编译器将源语言中的函数调用转换为机器代码中的函数调用。递归,虚拟方法和庞大的大小是内联并不总是可能/可行的一些原因。(另一个原因是构建的意图,例如单独的编译(对象模块),单独的加载单元(例如DLL))。
使大多数标准库函数本征化也没有任何真正的优势(这将使更多的知识硬编码到编译器中,而没有真正的优势),因此再次进行机器代码调用通常是最合适的。
C是一种著名的语言,可以说它省略了其他支持标准库功能的显式语言语句。尽管库已经存在,但该语言已从标准库功能转移到做更多的工作,而不再是该语言语法中的明确声明。例如,其他语言的IO经常以各种语句的形式提供其自身的语法,而C语法未定义任何IO语句,而是仅遵循其标准库以提供所有这些内容,这些都可以通过函数调用来访问,编译器已经知道该怎么做。
这仅仅是为了使语言本身尽可能简单。您需要区分语言的功能,例如循环的类型或将参数传递给函数的方式等,以及大多数应用程序所需的常用功能。
库是可能对许多程序员有用的函数,因此它们被创建为可共享的可重用代码。标准库被设计为程序员通常需要的非常常见的功能。这样,编程语言立即可用于更广泛的程序员。可以在不更改语言本身核心功能的情况下对其进行更新和扩展。
PHP
例如,它的广泛语言功能和语言本身几乎没有任何区别。
include
,require
和require_once
,if / for / while(结构化编程),异常,单独的“错误值”系统,复杂的弱类型规则,复杂的运算符优先级规则以及on and on 。将其与Smalltalk,Scheme,Prolog,Forth等的简单性进行比较;)
除了已经给出的其他答案之外,将标准函数放入库中也是关注点分离:
解析语言并为其生成代码是编译器的工作。包含已经可以用该语言编写并作为库提供的任何内容不是编译器的工作。
标准库的工作(总是隐式可用)是提供几乎所有程序都需要的核心功能。包含所有可能有用的功能不是标准库的工作。
可选标准库的工作是提供许多程序无法提供的辅助功能,但是这些功能仍然是非常基本的,对于许多应用程序需要保证标准环境的运输也是必不可少的。包含所有已编写的可重用代码不是这些可选库的工作。
用户库的工作是提供有用的可重用功能的集合。包含所有已编写的代码不是用户库的工作。
应用程序源代码的工作是提供实际上仅与该一个应用程序相关的其余代码位。
如果您想要一种适合所有人的软件,那么您将获得极为复杂的东西。您需要进行模块化以将复杂性降低到可管理的水平。并且您需要模块化以允许部分实现:
在单核嵌入式控制器上,线程库毫无用处。允许此嵌入式控制器的语言实现不包含pthread
库只是正确的选择。
在没有FPU的微控制器上,数学库毫无用处。同样,不必强迫提供类似的功能,sin()
对于该语言的语言实现者来说,使该微控制器的工作变得更加轻松。
在编写内核时,即使是核心标准库也毫无用处。您不能实现write()
没有系统调用到内核中,你无法实现printf()
无write()
。作为内核程序员,提供write()
syscall 是您的工作,您不能仅仅期望它存在。
不允许标准库中有此类遗漏的语言根本不适合许多任务。如果希望您的语言在不常见的环境中灵活使用,则在包含哪些标准库时必须灵活。您的语言对标准库的依赖程度越高,对其执行环境的假设就越多,从而将其使用限制在提供这些先决条件的环境中。
当然,诸如python和java之类的高级语言可以对其环境做出很多假设。而且它们倾向于在其标准库中包含很多东西。诸如C之类的低级语言在其标准库中提供的内容少得多,并且使核心标准库更小。这就是为什么您可以找到适用于几乎所有架构的C编译器,但可能无法在其上运行任何python脚本的原因。
编译器和标准库分开的一个重要原因是,它们具有两个不同的用途(即使它们都是由相同的语言规范定义的):编译器将高级代码转换为机器指令,而标准库提供了经过预测试的常用功能的实现。就像其他软件开发人员一样,编译器编写人员也重视模块化。实际上,一些早期的C编译器进一步将编译器拆分为单独的程序,以进行预处理,编译和链接。
这种模块化为您提供了很多优势:
从历史上讲(至少从C的角度来看),该语言的原始,预标准化版本根本没有标准库。操作系统供应商和第三方通常会提供充满常用功能的库,但是不同的实现包含不同的内容,并且它们之间很大程度上不兼容。在对C进行标准化时,他们定义了一个“标准库”,以协调这些不同的实现并提高可移植性。C标准库是与语言分开开发的,就像Boost库针对C ++一样,但后来被集成到语言规范中。
特殊情况的其他答案:知识产权管理
值得注意的例子是.NET Framework中Math.Pow(double,double)的实现,该框架是Microsoft从Intel购买的,即使该框架是开源的,也没有公开。(确切地说,在上述情况下,它是内部调用而不是库,但这个想法成立。)与语言本身分离的库(理论上也是标准库的子集)可以使语言支持者在绘制语言时更具灵活性。保持透明与必须保持透明之间的界限(由于与第三方的合同或其他与知识产权相关的原因)。
Math.Pow
没有提到购买任何东西,也没有任何有关Intel的信息,它谈论的是人们在阅读该功能实现的源代码。
这是一个很好的问题!
例如,C ++标准从不指定应在编译器或标准库中实现的内容:它仅指的是实现。例如,保留符号由编译器(作为内在函数)和标准库两者互换定义。
但是,我所知道的所有C ++实现都将具有由编译器提供的尽可能少的内在函数,以及由标准库提供的尽可能多的内在函数。
因此,尽管在编译器中将标准库定义为内部功能在技术上可行,但实际上却很少使用。
让我们考虑一下将某些功能从标准库转移到编译器的想法。
好处:
缺点:
std
)变得更加困难。这意味着,无论现在还是将来,将某些东西移交给编译器都是很昂贵的,因此需要一个可靠的案例。对于某些功能,这是必要的(不能将其编写为常规代码),但是即使那样,也需要提取最少的通用部分以移至编译器并在标准库的顶部进行构建。
作为我自己的语言设计师,我想在这里回应一些其他答案,但是请正在构建语言的人的眼中提供。
完成将所有内容添加到API中后,API尚未完成。完成所有可能的工作后,API即告完成。
必须使用某种语言来指定编程语言。您必须能够传达使用您的语言编写的任何程序背后的含义。这种语言是非常难写,甚至更难写。通常,它往往是一种非常精确和结构良好的英语形式,用于向计算机,而不是其他开发人员(尤其是为您的语言编写编译器或解释器的开发人员)传达含义。这是C ++ 11规范[intro.multithread / 14]中的示例:
相对于M的值计算B,原子对象M上可见的副作用序列是按M的修改顺序排列的最大副作用子序列,其中第一个副作用相对于B可见,并且对于每种副作用,并不是B发生在它之前。由评估B确定的原子对象M的值应是通过某些操作存储在M相对于B的可见序列中的值。[注:可以表明,一个值的可见副作用序列鉴于以下一致性要求,计算是唯一的。—尾注]
死了!凡是愿意了解C ++ 11如何处理多线程的人都可以理解为什么措词必须如此不透明,但这并不能原谅它是如此……那么……如此不透明!
与std::shared_ptr<T>::reset
标准的库部分中的定义进行对比:
template <class Y> void reset(Y* p);
效果:相当于
shared_ptr(p).swap(*this)
那有什么区别呢?在语言定义部分,作者不能假定读者理解语言原语。一切都必须用英文散文仔细说明。一旦到达库定义部分,就可以使用该语言指定行为。这通常容易得多!
原则上,可以在规范文档开始时从基元平稳地构建,直到定义我们认为的“标准库功能”,而不必在“语言基元”和“语言基元”之间划清界限。 “标准库”功能。在实践中,这条线被证明具有极大的价值,因为它使您可以使用旨在表达这些语言的语言来编写该语言的一些最复杂的部分(例如必须实现算法的部分)。
我们确实看到了一些模糊的线条:
java.lang.ref.Reference<T>
可以只由标准库类被继承java.lang.ref.WeakReference<T>
java.lang.ref.SoftReference<T>
和java.lang.ref.PhantomReference<T>
因行为Reference
是如此之深与Java语言规范,他们需要把一些限制进入的“标准库”类来实现这一过程的一部分缠绕。这是对现有答案的补充(对于评论来说太长了)。
使用标准库至少还有其他两个原因:
如果库函数中包含特定的语言功能,而我想知道它的工作方式,则可以阅读该功能的源代码。如果我要提交错误报告/补丁/拉取请求,通常编写修复和测试用例并不难。如果在编译器中,则必须能够深入研究内部结构。即使使用相同的语言(应该如此,任何自重的编译器也应该是自托管的),编译器代码与应用程序代码完全不同。找到正确的文件可能要花费很多时间。
如果您走那条路,那么您将与许多潜在的参与者失去联系。
许多语言在某种程度上都提供了此功能,但是热重载正在执行热重载的代码将极其复杂。如果SL与运行时分开,则可以重新加载它。
这是一个有趣的问题,但是已经给出了许多很好的答案,所以我不会尝试完整的答案。
但是,我认为没有引起足够注意的两件事:
首先是整个事情不是超级明确的。确切地说,这是有点频谱,因为我们有理由做不同的事情。例如,编译器经常了解标准库及其功能。示例示例:C的“ Hello World”函数-printf-是我能想到的最好的函数。这是一个库函数,必须排序,因为它非常依赖平台。但是编译器需要知道它的行为(定义实现),以警告程序员错误的调用。这并不是特别整洁,但被视为一个不错的折衷方案。顺便说一句,这是对大多数“为什么设计”问题的真正答案:很多折衷方案,“在当时看来是个好主意”。并非总是“这是明确的方法”或“
其次,它允许标准库不完全是该标准。在很多情况下,希望使用一种语言,但通常伴随它们的标准库既不实用也不理想。在非标准平台上,最常见的情况是使用系统编程语言(如C)。例如,如果您的系统没有操作系统或调度程序,那么您就不会有线程。
使用标准库模型(并支持其中的线程),可以轻松地进行处理:编译器几乎相同,您可以重用适用的库中的某些位,而所有不能删除的内容都可以删除。如果将此内容编译到编译器中,则事情开始变得混乱。
例如:
您不能成为兼容的编译器。
您将如何表明您偏离标准。请注意,通常会有某种形式的import / include语法可能会失败,例如,如果标准库模型中缺少任何内容,则python的import或C的include很容易指出问题。
如果要调整或扩展“库”功能,也会遇到类似的问题。这比您想像的要普遍得多。只是坚持使用线程:Windows,Linux和一些外来网络处理单元都完全不同地执行线程。尽管linux / windows位可能是相当静态的,并且可以使用相同的API,但是NPU的内容将随着星期几和API的变化而变化。如果没有办法将这种事情分解出来,那么编译器将很快偏离人们的决定,因为他们很快决定需要/不可以支持哪些位。