C++:标准化之旅的终点站

— Bjarne Stroustrup专访

马皓明 翻译  荣耀 指导

C/C++ Users Journal:我知道您拥有应用数学博士学位,您还有其他学位吗?

Bjarne Stroustrup:不尽然。我在丹麦Aarhus大学获得了“计算机科学数学”硕士学位。我选择数学方向是因为那是当时研究计算机科学的唯一途径。我在剑桥(当然在英国)获得了计算机科学博士学位。我是个很不怎么样的数学家,但我猜总比不是要好一些。

CUJ:您是怎样进入计算领域的?

BS:我大学时报的是数学与计算机科学专业,然后就进了计算领域。我曾努力回想当时为什么那么做,但真的想不起来了。在填报之前我连一台电脑都没见过,我猜我是被它的科学性与实用性所吸引了。

CUJ:您是什么时候、又是如何注意到C with Classes这个东西即便不是您的职业(换句话说,不单纯是一个暂时兴趣),也将占据您大量的时间?

BS:大概是在1982年,我意识到对C with Classes用户社群提供支持变成了我个人精力无法承担的工作。就我看来,用户人数之多已超过一个人在从事科研的同时还能胜任这份工作的程度,但还没多到足以支撑起一个基础组织。我必须决定是把C with Classes发展为一门更具灵活性与表达力的语言,还是放弃它。幸运的是,我没有考虑过第三种选择 — 人们称之为惯用方案 — 通过大肆宣传拉拢更多用户。我决定将自己涉猎的源自C与Simula的宝贵思想在C with Classes中进一步结合发展,其结果就是C++。后来有几次我试图“逃脱”去做别的工作,然而C++社群的成长与新应用带来的挑战却一直使我难以脱身。

CUJ:您目前日常工作是什么?

BS:我在努力筹划一个重点研究大规模编程的团队。确切地说,研究内容是大规模程序中的编程技术运用问题,而不只是语言设计和小规模(学生)程序的问题,也不只是把重点集中在设计和(或)过程上。我认为编程技术、编程语言以及程序员个体这三方面在大规模系统开发中扮演中心角色,而工业性项目的规模以及编程扮演的角色却往往被忽视。这项研究将涉及库与工具方面的工作。

当然了,C++将在此担任一个主要角色,但我从没打算把自己的兴趣局限于C++。我所在的部门是AT&T研究所的一部分。AT&T分解后,保留了老AT&T贝尔实验室信息科学研究所的一半(另外一半以及名称归朗讯科技公司所有)。属于AT&T的部分现在被称作AT&T研究所,我们的目标是成为首屈一指的研究组织。在很大程度上,C++仍然是我的“日常工作”。我为标准化议题工作,帮助用户,撰写论文,发表讲演,撰写书籍,等等。偶而我甚至还会写些代码 — 尽管没有我希望的那么多。

CUJ:在您“退休”之前,您希望完成什么?

BS:从C++的发展中退休,还是真的退休?我还不到退休的年龄,因此我设想你是从C++的角度说的。如果标准委员会在斯德哥尔摩的会议达到了我对此次会议期望的话,我便认为语言及标准库是完备健全的了,C++演化的这个阶段就告一段落。除非有什么事发生严重差错(到你发表这篇访谈时我们就会知道结果),ISO C++将会比任何早期版本都更接近我的理想。对模板的改进以及将STL包含进标准库是使C++更符合我希望的关键。

CUJ:您希望在斯德哥尔摩能实现什么目标?在您看来,有什么事可能发生严重差错,那会造成什么影响?

BS:我提到的问题是指模板的分离编译。ARM、TCPL第二版以及最初的标准草案工作报告都允许模板的分离编译,但不允许一个非内联模板函数被包含进一个以上的编译单元中,那将导致实现效率低下,所以“将非内联模板函数定义包含进头文件”的做法变得很普遍,因此对标准草案也进行了修改以允许这种做法。常见问题以及最初的折衷在D&E中有一些细节描述。

我感觉过分狂热的“简单”方案主张之一是,所有可能需要实例化一个模板的情况下,用户都“简单地”用#include包含进所有需要的信息,因此某些委员会成员提议禁止模板的分离编译。在Santa Cruz,这个提议几乎被接受了(不顾我强烈反对),而在斯德哥尔摩它也有可能被接受(事实上没有)。这将是委员会第一次禁止一项基本特性。

禁止模板分离编译的理由有多种:它难以实现,导致编译速度慢;难以使用;难以准确特化;出错时难以提供切中要害的错误信息;等等。提倡分离编译的原因包括:更好的接口与实现分离;维持语言其他方面已有的标准分离编译;可能加快极大规模程序的编译速度。先不谈语言设计者出自技术角度的理想化考虑,分离编译已经成为所有用于大型程序设计的语言的一个组成部分。

请考虑清单1所示程序。有点摸不着头脑,是吗?这并不是我所说的优秀编程,但它是耗费标准委员会大把时间的那种乖戾示例。每个程序的合法性以及意义必须受到约束。把sum的定义含入用户代码,sum.c中的声明将影响user.c中名字的含义(例如,INCR与count),还会影响模板声明用到的名字的含义。对那些依赖头文件里大量信息的程序而言,各种千奇百怪的副作用都有可能产生。特别是当用#include将一个文件含入另一文件时,没有任何保护机制禁止(在被包含文件中)使用(与包含它的文件中相同名字的)宏。另一种做法就是用分离编译机制将模板定义的情境尽可能与使用该模板的情境相分离,如清单2所示。

清单1:通过置入式模型使用模板

// sum.c

#define INCR 2
void trace(void*);
static int count;
template<class T> T sum(vector<T>& v)
{
    count += INCR;
    trace(v);
    T r = 0;
    for (int i = 0; i < v.size(); i++)
        r += v[i];
}

// user.c (inclusion style)

#define trace my_tracer
#define INCR(v) (v)++
extern int count;
#include "sum.c"
void g(vector<int>& vi)
{
    int s = sum(vi);
}

// End of File

清单2:通过分离式模型使用模板 

// user.c (separation style)

#define trace my_tracer
#define INCR(v) (v)++
extern int count;
template<class T> T sum(vector<T>& v);
void g(vector<int>& vi)
{
    int s = sum(vi);
}
// End of File

在置入式模型中一些“名字漏洞”方面的问题可以通过名字空间来弥补,但不能解决宏引起的问题。分离式模型需要对“如何从相关情境中选出名字”这个问题多加注意。然而,复合来自不同源文件的模板的程序,与复合来自分别的名字空间中的模板的程序,所需面对的情境与名字绑定有关的问题大体相当。这是自然而然的,因为名字空间与编译单元是表达逻辑分离的两种方式,编译器在两种情况下必须面对类似的挑战。

目前委员会陷入了僵局。然而,我认为有希望成功的工作正在进行。主要思想就是简化查找规则以至达到可将模板用到DLL里的程度。这大体上意味着对实例化情境的依赖应该以全局和名字空间符号表的形式来表达,因此,当实例化一个模板时无需再检查这个编译单元实际的源代码 — 尽管通过检查可能产生出更优化的代码。这是个诡异但十分重要的议题。我认为缺少模板分离编译机制的C++是不完整的。我期待出现比目前分离式和置入式两种模型在技术上更优秀的解决方案。

CUJ:您提到的“对模板的改进”是什么,为什么会那样?

BS:ARM详细阐明了模板的一个相当局限性的版本。当时我担心定义的内容难以高效实现,因此强加了一些希望在日后放松的限制。几年以来为了适应使用模板时出现的问题,对这些限制已做出放松。考虑一个list:

template<class T> class list { ... };

除非编译器聪明非凡,不然它会为list的每一个实例复制一份list函数的代码。例如:

list<int> li;
list<complex> lc;
list<shape*> lps;
list<char*> lpc;
list<node*> lpn;

将会生成list代码的五份拷贝,这对项目来说可能是一个实实在在的问题。一个项目中出现几十种不同元素类型的list并不稀奇,这将导致严重的代码膨胀。我在关于模板的第一篇论文中推荐的解决方案是对所有类型的指针list使用单独一份共享实现。例如:

template<> class list<void*> { ... }; // specialization
class plist : private list<void*> { ... };

现在每份指针list(plist)都通过一个void* list实现,这样就避免了代码膨胀。不幸的是,用户现在必须记住区分list和plist。经验表明程序员做不到,结果仍发生代码膨胀。我们需要这样一种方法:既能为指针list定义一个专用(共享的)实现,又不必引入一个新名字。也就是说,提供一份特别的实现给模板参数恰好是指针类型的list专用。这可以通过偏特化来表达:

template<class T> class list<T*> : public list<void*> { ... };

<T*>意味着模板参数为指针的每一个实例都将使用这份特化实现。这使得用户在变化list的实现时无需影响list的接口。有几项针对模板的这种细微扩展,它们联合起来就使C++表达能力得以重大改进。在此,我仅提一下模板参数缺省值。举个例子,请考虑比较两个由任意字符组成的string的大小。一个(过分)简单的版本看起来像这样:

template<class T>
int cmp(string<T>& a, string<T>& b)
{
    for(int i = 0; i < a.length() && i < b.length(); i++)
    if (a[i] != b[i]) return a[i] < b[i];
    return b.length() - a.length();
}

问题在于,针对某种字符类型T,operator<可能没有定义。另外,对于不同的string类型,你可能想使用不同的比较准则。例如对普通字符string可以有大小写敏感以及不敏感两种比较。尽管传统的明显解决方案是令比较准则为一个函数的参数,但一种更优雅且高效的方法是令比较准则为一个模板参数:

template<class T, class Comp = CMP<T> >
int cmp(string<T>& a, string<T>& b)
{
    for(int i = 0; i < a.length() && i < b.length(); i++)
    if (!Comp::eq(a[i],b[i])) return Comp::lt(a[i], b[i]);
    return b.length() - a.length();
}

自然而然,定义缺省的比较准则CMP以支持普通的比较运算:

template<class T> class CMP {
public:
    static bool eq(const T& a, const T& b) { return a == b; }
    static bool lt(const T& a, const T& b) { return a < b; }
};

一个大小写不敏感版本可能像这样定义:

template<class T> class ONE_CASE {
public:
    static bool eq(const T& a, const T& b) { return toupper(a) == toupper(b); }
    static bool lt(const T& a, const T& b) { return toupper(a) < toupper(b); }
};

于是我们就可以这么写:

string<char> sc;
string<char> sc2;
int c = cmp(sc, sc2); // 大小写敏感的比较 
int c = cmp<char, ONE_CASE>(sc, sc2); // 大小写不敏感的比较

cmp<char, ONE_CASE >这个符号明确规定了调用哪个模板函数。当没有显式特化模板参数时,就从调用或从模板声明的默认指定推导出来,因此

cmp(sc, sc2);

等价于

cmp< char, CMP<char> >(sc, sc2);

通过模板参数比通过函数参数提供“策略”更好的原因在于:由模板参数提供的操作能够很容易地实现内联。我认为关键之处在于,这些扩展虽小,但它们对基本编程和设计理念提供了直接支持,例如提供多种适当的实现而无需影响用户接口,提供参数化机制以便在大多数常见情况下无需用户做任何特别指定。

CUJ:您如何概括STL带来的影响与益处?

BS:我最大的愿望是它能教更多的人把C++用作一门高阶语言而非一门美化了的汇编语言。我对C++的目标曾经是 — 现在也是 — 提高代码的抽象级别以便程序文本在任何合理的地方都能直接反映应用的概念以及计算机科学的一般概念。但是,有太多的人迷失于与位(bits)、指针的无谓纠缠之中。标准库允许人们使用string、vector、list和map等既有内容,从而将使用C风格数组和指针等低阶设施实现基础数据结构的烦忧推迟到必要之时。

CUJ:您经常用Vasa的故事鼓励采用简单的途径定义C++语言。但“C++是现存的最复杂的语言之一”却是更普遍的大众观点。您对此有什么评论吗?C++有不同的“使用层级”吗,您如何界定它们?

BS:显然存在这么一个危险,否则我为何找麻烦去讲一个警戒故事呢?在Gustav国王的旨意下,Vasa的建造始于1625年。最初打算将它建成一艘正规的军舰,但在建造过程中国王在其他地方看到了更大更好的船,于是他改变了主意。他坚持建造一艘拥有双射击甲板的旗舰,同时还坚持在上面配备大量的雕像以障显皇家旗舰的身份。其悲剧后果给人的印象很深刻,尽管不是最最深刻的。在Vasa的处女航行中,在穿越斯德哥尔摩海港的中途就被一阵风吹翻了,它的沉没夺去了50人的生命。

它的残骸被打捞上来,今天在斯德哥尔摩博物馆里就能看到。它被视作一件尤物 — 比它最初未扩展的设计优美得多,假若它遭遇的是17世纪军舰的普通命运,那它远不会像今天这般优美,但这并不能作为对其设计者、建造者以及预定使用者的安慰。

到目前为止,C++逃脱了Vasa的命运 — 它并没有倾覆并消失 — 尽管有很多痴心妄想和很多可怕的预言。一些较新的“扩展”被视为确实使C++程序员生活更轻松的一般化措施。这几年来,我的程序确实变得更短小、清晰并更容易编写了。为了表达优秀的设计理念我不必再像以前那样绕许多弯子了。诚然,假若你认为最棒的编程语言得能够让一个新手把一个演示程序跑得飞快的话,C++还没发展到那么好,因为掌握它确实需要比较久的时间。然而,我关心的东西主要是产品代码,在胜任有余的人手中,C++是一个快乐的选择。

ISO C++比ARM中描述的C++更接近我的理想。可能值得注意的是,Gustav国王有一个方面是正确的:假若Vasa的建造沿着最初的设计路线 — 一艘“现代的”双射击甲板的军舰,那它可能随着建造的真正完工就沉没在海底了。缺陷并不在于增添一个射击甲板 — 那个射击甲板对于Vasa履行自己的使命是必需的,缺陷源于添加这个射击甲板的方式是错误的。站在这个角度,我们或许就会以一种更宽容的观点看待委员会花费过量的时间了,那是为了确保C++的扩展设计合理、可靠。

比方说,拿最初的Pascal语言进行比较C++当然非常复杂,但任何其他现代编程语言都是如此。然而,与我们使用的编程环境和我们编程服务的系统比较而言,C++还算是简单的。对一个新手来说,企图一开始就学会C++的所有方面然后着手使用之,是严重错误的。换种方式,最好先从一个子集入手,然后随需要扩充自己的技能。我建议从一个类似C的子集入手,但要使用标准vector、list、map和string来代替与C风格数组和字符串的无谓纠缠。当然,宏是应该避免使用的,union和位操作也是如此。然而,用C和C++的这个子集,加上我提到的标准库里的那四个类,你就可以写出非常漂亮的代码。

不久之后,你可以开始试验使用简单的类、简单的模板和简单的类继承层次。我建议在设计任何非无足轻重的类继承层次时都要把重点集中在抽象类上。在学习初期,非常简单的异常处理机制也可以派上用场。最重要的是要牢记把重点放在要解决的问题上,而不是C++的语言技术方面。如果你能得到一位有经验而且不怀偏见的朋友或同事的帮助,学习一门语言时总会省不少劲。

CUJ:C++演化的下一步是什么?

BS:工具(开发环境)和库的设计。我希望看到更多的C++编译器与连接器。在一个中等大小的C++程序里,将少数几个函数局部化,重新编译并连接这个程序的时间会相应缩短两秒左右。我希望看到这样的浏览器和分析工具:它们不仅懂语法,还了解程序中所有实体的类型。我希望看到一种优化器:它确实注意了C++构件,并且较好地对之进行优化工作 — 不是把多数有用信息简单地抛掉而只把剩余部分丢给一个主要只理解C的优化器处理。我还希望看到集成有增量编译器的调试器,以便更接近一个C++解释器。(我也希望看到一个优秀的可移植的C++解释器)所有这些都不是科学幻想,实际上,我建议的这些东西中的多数都已有了试验版本(以及更多的东西)。我们还在忍受着第一代C++开发环境与工具的折磨。

CUJ:C++的成功很大程度上当然是因为它建立在C之上。(我们都熟悉“与C尽可能接近但不要更近”这句惯用语,它是爱因斯坦的“尽可能简单但不要更简单”的胞兄弟)但是与C兼容已然成为一个挑战,并导致即将出台的ISO标准在演化过程中发生多处妥协折衷。其中您能回忆起的最主要的折衷是什么?

BS:我想多数折衷发生在标准委员会成立很长时间之前。(请参考我对下个问题的回答)诸如类、名字空间、模板以及异常这些C++专有的主要特性之所以受到很大限制,并非因为什么C特定的东西,而是为了能够生成非常紧凑且高效的代码,并能与其他语言代码共存。实际上,多数折衷可被看作是由“零开销原则”驱动的,所谓“零开销”,即你无需为你没用到的任何特性付出任何空间或时间代价。这条原则使C++成为一门可行的系统编程语言,并使它避免发展成为便于开发玩具例程却不适合作为一个有用的日常编程工具的东西。

CUJ:暂时假设C兼容性不再是一个议题。以您的观点,C++会有什么不同?

BS:这真是一个不公正的问题,因为“与C尽可能接近但不要更近”确实是一项基本设计目标,给C++使用者带来无限技术性好处。那并非只是一个政治性或商业性(广告性)的决定。假设没有C或者C超出了我的需要,我也会另外找到某种语言与之兼容。我看不出再设计一门Algol家族的语言会有任何价值。

另外,为了寻找并维持与C兼容合适的度,我们付出了彻底的努力,其价值不容低估。C兼容性是最难确定并付诸实践的决定之一,而且也是最重要的决定之一。然而,C有很多我不喜欢的方面,假若存在另外一门语言没有那些讨厌特性,并且与C一样高效、灵活、可用,我就会选择与它兼容,而不选择C。例如,我认为C的声明语法是一个失败的试验品,总的来说,C的声明语法过于宽松。请注意C++抛掉了C里“隐式int”规则:

static T;

C++中不再合法。如果你确实想声明一个整型就必须写成

static int T;

有许多这类细节问题,它们给写编译器的人平白增添了许多困难与麻烦,也给现实生活中C和C++代码(即不像上边例子那样无足轻重的代码)的普通读者增添了不必要的烦恼。我猜大家都知道我对预处理指令的厌恶。Cpp对于C编程是必不可少的,而且在传统C++编译器里仍然重要,但它是一匹“驽马”,多数依赖它的技术也都如此。“使Cpp变得多余”一直是我的长期目标。然而,在它变得纯粹多余(而这还没有实现)很长时间之后,我才能梦想将它废除掉。

template、const、inline以及名字空间让多数对宏的使用都变得多余(它还会起反作用),到目前为止我们还没有替代#ifdef的广泛可用的方案。预处理指令是导致更成熟的C程序开发环境缺乏的主要因素之一:程序员看到的源文件并非编译器看到的文本,这是一个致命障碍。我想到了应该严肃看待无宏C++编程的时候了。

我还认为C风格数组对于多数用户来说过于低阶。然而,我认为C的成功并非偶然。对许多项目来说,C作为一个明智选择在许多方面比其他替代语言优越,曾经如此 — 而且今天往往依然如此 — 当然,除非替代语言是C++。除了缺少可用的C++编译器的情况,我从没发现过任何应用有任何理由使它选择C比选择C++更好。如果某种C没有的C++特性在一个项目中不适用,置之不用就是了。

CUJ:您对Java革命持什么态度?

BS:什么Java革命?Java(至少)是两种截然不同的东西:一门极度模仿C++的还算标准的现代编程语言,一个相当有意思的能通过web浏览器下载代码至他人电脑的系统。后者会引发棘手、有意思而且重要的问题。如果Java与Javascript严重的安全问题能得到解决,这种能力可能会非常重要。即便安全漏洞还没被填补,它也会十分重要,因为不管怎么说似乎大多数人通常并不怎么关心安全问题。

我猜你打算问我的是对Java作为一门编程语言以及它与C++的关系的看法,但我不想就此说太多,因为若不以使用者的大量经验为基础,对语言的比较就很难做到公正。考虑到你提出的上一个问题,我想要指出的是,即使我不受兼容性议题的约束,Java也绝不是我想要设计的语言。但是,Sun公司在市场营销上投入的效果真是惊人。这是难忘的一课,对于程序员个体、小公司以及学术界而言是不祥的一课。如果人们坚持要比较C++和Java,我建议他们翻翻D&E,看看C++为什么会成为这个样子,并以我为C++制定的设计标准为基础来考虑这两种语言。C++与Java的区别并非仅停留于表皮,而且并非在所有方面其中一个都优于另一个。

CUJ:1989年12月您在X3J16的开场致辞上说过,如果委员会制订出一套标准花费的时间超过5年,它就会以失败告终。当一切尘埃落定时,近8年的时间将要过去。您有什么评论?

BS:我猜现在适宜说些“含糊之词”,因为我认为委员会完成了一项伟大的工作 — 尽管这项工作用的时间太长了。我严重低估了在差异极大的会员之间达成一项共识所需要的时间。然而,回头看几年之前,比如说1995年的3月 — 我预分配的五年时间结束之时,所有主要的语言特性与标准库设施都已就位。若不是因为模板编译发生了让人讨厌的事,如果以一种带有同情心的观点看,我们可以认为最后3年用在精化ISO规则上了。我希望并确实发生的一件事是:在书写标准笔墨未干之前,标准内容早已体现在编译器上了。标准化工作的许多优秀内容已经到了C++程序员的手中。

CUJ:一份标准的出现对C++社群会有怎样的影响?对您个人而言呢?

BS:对于社群:稳定性,更优秀的编译器、工具和库,更好的教学内容,还有更棒的技术。对我自己:终于获得遂愿使用C++的机会,不再因标准化工作和对语言设计的关注而分散精力。重要且有趣的话题是编程,而不是编程语言。我发现有太多人属于下面两个阵营之一:一个阵营认为编程语言无关紧要,它们只会成为系统构建者的障碍(许多C程序员属于此阵营),另一阵营认为编程语言可以为他们创造奇迹 — 只要一些非常特别的语言技术方面“准确到位”(但它们从未“准确到位”过,因此所有努力都用在设计这种完美的编程语言上了)。

而我属于第三个阵营:我知道一门优秀的语言对于程序员个体和团队都有极大的帮助,但使用被认为糟糕的语言写成的优秀代码比使用声称优秀的语言写成的更多!至关重要的是程序员对“待以解决的问题以及解决它所需要的技术”的理解。编程语言可以帮我们表达清晰的思想,甚至通过提供一套合理的框架帮我们澄清“近乎正确”的思想。我认为C++提供这种帮助所服务的应用领域范围比其他任何现有语言都广。倘若人们将时间用在掌握ISO C++特性所支持的技术上面,那么C++将会成为一个更优秀的工具。然而,如果你对自己正在做的事以及如何去做没有一个清晰的概念,那么无论选择什么语言你都会迷失于其中。

后记

当本文刊出时,C++委员会在斯德哥尔摩的会议已经召开,且总体上取得了令人振奋的成果。为了回答这篇访谈提出的几个问题,Bjarne在斯德哥尔摩会议上做了如下注释:

斯德哥尔摩,委员会决定了关于废除模板查找规则和依赖规则的一项提议,这让语言更简单且规格更明确。在多数人看来,它回答了模板分离编译引起的问题,因此无须禁止分离编译了。我对这项决定(在ISO国际代表中支持与反对人数比为6比1)很满意。这项决议的大部分功绩归属于Silicon Graphics (SGI)的人们,他们开启了引发这项决议的工作,并致力于其细节问题。

许多人感觉在斯德哥尔摩敲定的议题的数量之多(库工作组尤其“高产”)必然造成对CD(Committee Draft,委员会草案)投票决议的推迟 — 因为会议结果产生的工作文件(Working Paper)中包含如此之多的变化。因此,委员会没投票表决是否提交(待以编写的)斯德哥尔摩会议的文档作为CD,但投票表决一致通过了以下决议:在十一月夏威夷会议中,把合并斯德哥尔摩决议与其编辑上的调整所得文档提交为CD,此文档中不会出现语言或库在技术上的进一步改动

难以估计对于进度表这意味着什么,因为那取决于其他组织和委员会(比如ANSI和ISO)相互影响的日程安排,但估计推迟的时间会在0到4个月之间变化。我认为重要的是,我们终于确切知道了C++语言及标准库包括哪些组成内容。当然了,还要做进一步改进,但我对我们现在所拥有的东西非常满意,是交付成果的时候了。正如谚语所讲:“最优秀是优秀的敌人”。尽管我们现在拥有的不一定完美,但它非常优秀了。我盼望着一个稳定并且高产的时期的到来。