[声明]:本英文资料源自于Herb Sutter 创建的“Conversation”栏目,“C++ 翻译小组”的翻译作品供学习交流与参考用途,不得用于任何商业用途。未经Herb Sutter、Jim Hyslop同意,不得转载;对于违反以上条款,翻译小组对此不负任何责任;特此声明。
文章来源:http://www.gotw.ca
版权归属:Herb Sutter and Jim Hyslop
译 者:徐波
对话:#11: 根源
这个方塔比在录像中看到的还要大一些。
我站在人造悬崖的边缘,它处于挖掘现场的的边缘,旁边是一层稍呈褐色的冰。方塔朝着我的那一面看上去象是纯水晶,表面散发着一种光辉,徐徐向上,在大约距我脑袋十二米的地方是锥形的塔尖,仿佛是指向那儿的指针。再上去一点,淡灰色的塔顶内表面罩在我们头顶,覆盖了整个区域。
远处隐隐传来取暖器的嗡嗡声,尽管衣服穿得并不少,我仍然感到一丝寒意。我们呼吸的气息静静地停滞在我们面前的空气中。气温还算能够忍受,但仍低于零度很多,冰是不会融化的。
在我的下面,石塔稍显宽阔,延伸二十几米后到达它与顶部联接处,只有部分已被挖掘能够看到,那个更宽阔的建筑,我知道它是一座建筑的屋顶,几乎完全被冰所覆盖。仅根据我下面所看到的屋顶,无法用视觉来判断这个建筑的尺寸,但我根据测声认为它钻得相当深,很可能直达冰层的下面。
“入口在远处那边的下面”,我的同伴说,“是个大家伙吧?” 我的思绪又回到了当前,点了点头,微微冲她一笑。我们向下走到挖掘现场的地面上,绕着放在那儿的机器以及两三个一组工作的人们缓步而行。
石塔底部的入口是唯一属于人造的东西。它的边缘很粗糙,不久前还是一堵墙,很明显不是这个古代建筑原先设计的一部分。
入口有点暗,但在不远处有临时的照明设备。我当时肯定有些犹豫,因为我同件的手抓住我手臂,试图打消我的疑虑。“好了,”我笑着回头对珍妮说,“我并不是对一切都想刨根问底。”
“是的,哈!那是我们的管理员。”
- - - - - - - - - - - - - - - - - - - - - - - - -
“那么,我们午餐时再见。”我对安娜说,然后挂上了电话,她是我的新任女友。我们已经外出够长时间了,对于她是Guru的女儿,我感到很惬意,而在我们刚开始约会时我并不知道这一点。
“嗨,伙计!”鲍勃的叫声打断了我快乐的瑕想。“我想我告诉过你不要破坏结构。”
我回过头,用最耐心的声音回答:“鲍勃,你应该记得,我最后一次破坏结构是因为有些...是不是该这么说,你的模块中有不够理想的代码。”嗯,象往常一样,他又在呷他的咖啡。
“哈哈,这次是你的问题,伙计!”鲍勃啧啧有声,“经过的你修改,我的代码总是崩溃。”
我叹了口气,不想与他争执,“好了,鲍勃,告诉我问题出在哪里,我来看看。”
“这次肯定是你的缘故,”鲍勃笑着说,表情却颇为不快,“搞清楚这儿谁的经验更丰富,伙计。不要以为从古怪的Guru那儿吸点营养就能指手划脚。再说,对于我,她的权威实在有限。”他写下问题文件的名称后就走了。我只能听天由命,开始检查这个模块。
一小时后,我还是没理出个头绪来。“噢!这对我来说太奇怪了,”我喃喃自语。这看上去象是我自己代码的问题——鲍勃的代码在原先的类上工作良好,但在我修改过的类上工作却有问题。
看上去类层次结构相当简单:
class parent
{
public:
virtual void f();
// etc...
};
class child : public virtual parent
{
public:
void f();
};
我所做的修改之一就是把child虚拟地继承于parent,使它在这个类层次结构的任何地方都能使用。我尽了最大努力,但看上去我别无选择——我打算深入挖掘鲍勃的代码了。“又要搞破坏了,亲爱的朋友”,我嘀咕着,设置了一个断点,准备单步跟踪这个函数。经过半小时的严格调试,我从一团杂乱中找到了问题的根源:
void parentPtrPtr(parent **b)
{
(*b)->f();
}
void childPtrPtr(child **d)
{
parentPtrPtr(reinterpret_cast<parent **>(d));
}
对于最后一个函数,我自有想法。我知道要谨慎对待cast,特别是reinterpret_cast。我去掉reinterpret_cast,不出所料,编译器卡在这行代码上,抱怨paraent**和child**是不相关的类型。
“你的怀疑是对的,我的学徒工。”当Guru柔和的声音在我身后响起时,我记得只有一次没被她吓一跳。这次也是如此,但没有太吃惊。“指向child的指针的指针,”她接着说:“不能被隐式转换为指向base的指针的指针。
我向后踢了踢腿,伸了伸懒腰,“我也这样想。鲍勃所做的一切好象就是为了避免编译器报错。我不理解为什么它能在原先的类上工作,但现在却会导致崩溃。唉,我甚至不明白为什么编译器不能做隐式类型转换。”
“你现在所体验的崩溃正是它不允许这么做的十足理由,”Guru理了理耳后一络银色的头发,“你的派生类虚拟地继承于基类。”我象一只在车灯前不知所措的鹿一样紧紧盯着她,让她知道我没搞明白。“考虑一种简单继承的情况,”她说,拿起一支笔,开始在我的白色书写板上写起来:
class parent { /* 不管怎样都行,但至少要有一个虚拟函数*/ };
class child : public parent { /* 随便怎样都行 */ };
“虚拟函数的一种典型实现,”她接着说,“在内存布局中,编译器首先会给类附加一个指向虚拟函数表(vptr)的指针,然后是parent类的数据,再接下来是child类的数据。”
parent::vptr
parent data
child data
“等一下,”我打断说,“你不是经常告诉我虚拟函数表并非标准所必需,而只是实现方案的细枝未节?”
“是这样,我的的孩子。神圣的标准并没有规定必须要有虚拟函数表,事实上,它甚至没提到它。然而,庄严的标准委员会的成员们的目标之一就是规范化现有编译器的实际情况。允许child** 隐式转换为parent**将在许多现有的编译器上造成问题。其中一个特别的问题就是我接下来要说的,如果你允许我说下去的话。”她严肃地看着我,我没敢说什么。
“这种布局方式意味着this所指向的地址可以既是基类又是派生类。使用我们所假设的编译器,this始终指向vptr。派生类的成员函数可以简单地通过this取得其数据成员的地址,只要加上一个包括基类大小和vptr大小的偏移量即可。
“然而,当你在这个混合体里再加上虚拟继承时,情况就变得更加复杂,考虑一下:”
class parent { /* whatever */ };
class child1 : public virtual parent { /* whatever */ };
class child2 : public virtual parent { /* whatever */ };
class multi : public child1, public child2 { /* whatever */ };
“显然,由于上面parent类的数据紧随child类的数据之后,编译器无法对中间类child1和child2采取和child一样的布局形式,因为虚拟继承规定只能出现基类数据的一份拷贝。解决方案——确切地说,解决方案之一就是在每个子对象的开始处附加一个vptr,如下所示:”
parent::vptr
parent data
child1::vptr
child1 data
child2::vptr
child2 data
multi::vptr
multi data
“这样,当编译器触发各种成员函数时,它可以动态地调整隐式的this参数,从而使成员函数能够正确地访问成员数据。”
“哦!我明白你的意思了,”我说,“当你有一个multi对象时,指向它的指针的值有时会稍有不同,这取决于它是指向parent子对象还是child1子对象。这也就解释了为什么中间类必须使用virtual关键字——我以前一直认为这是个错误,而multi应该是使用virtual的类。所以,不管怎样,让我们回到原来的那个问题。这就是说……嗯……不,我还是不太明白,这跟指向指针的指针又有什么关系呢?”
她慢条斯理地说:“让我们给这些值分配任意的内存地址,假定每个类恰好包含一个整型数,这样整型数和指针的大小都是4个字节,不存在填充和对齐问题。让我们再加上一些代码,举几个multi类的实例:”
void f()
{
multi m;
multi *pm = &m;
multi **ppm = ±
//...
Address
Description
Value
0x1000
parent::vptr
?
0x1004
parent data
?
0x1008
child1::vptr
?
0x100c
child1 data
?
0x1010
child2::vptr
?
0x1014
child2 data
?
0x1018
multi vptr
?
0x101c
multi data
?
0x1020
Pm
0x1000
0x1024
Ppm
0x1020
“编译器将multi对象地址的第一个字节作为multi指针所指向的地方,也就是0x1000。现在假定你往这个函数里再添加一些变量:”
//...
child1 * pc1 = &m;
child1 ** ppc1 = &pc1;
//...
“编译器简单地取m的地址,将它调整为正确的子对象(在这里,也就是child1子对象的起始处),把它赋值给pcl。ppcl只是简单地包含pc1的地址:”
Address
Description
Value
0x1028
Pc1
0x1008
0x102c
ppc1
0x1028
//...
“但现在假定你想把ppm赋值给ppcl:”
//…
ppc1 = ppm; //该怎么处理呢?
//…
“假定编译器允许你把装在ppm内的值直接赋值给ppc1,ppc1的值就会被设为0x1020,也就是pm的地址,pm指向的是multi的基地址0x1000。当你对ppc1进行“提领”(dereference)操作时,你希望得到一个指向child1指针,但事实上你得到的却是指向parent的指针(或指向multi的指针,它们在我们假定的编译器里有相同的基地址。)”
“我明白了,”我惊叹道,“但等一下,为什么它在原来的类里没问题呢?”
“在非虚拟的继承里,这个特定的编译器把相同的基地址赋值给child和parent子对象。本质上,这两个地址的比特模式(bit-pattern)恰好相同,所以reinterpret_cast碰巧能正确工作。”
“太好了,”我说,“我明白这个道理了。现在我该怎么解决这个问题呢?”
“记住这句话‘每个问题都可以通过附加一个间接层来解决’。现在这种情况下,我的孩子,我们的间接层已经太多了。”
“太多的间接层,”我陷入沉思,突然,灵光一闪,“去掉一个间接层!”我迅速地在白色写字板写了一些代码。
void
childPtrPtr(child **d)
{
parent *pp = *d;
parentPtrPtr(&pp);
}
“很好,我的孩子。当你将child*赋值给parent*时,就会对this参数进行调整。现在,parentPtrPtr函数得到的是正确的指向parent的指针的指针。”
“而且很及时,”我说,看见安娜正在走过来,“我有个饭局。”Guru优雅地起身告辞。这时,鲍勃晃了过来,他正去取新鲜的咖啡。他向安娜打了打招呼:
“嗨!小甜甜,什么风把你吹来了?一块儿吃午饭吧。”
“下次吧,爸。我约好了跟男友一块吃午饭。”安娜指指我。
我惊骇地盯着鲍勃,他也以同样的眼神盯着我。我们不约而同地转向安娜,脱口而出:“他是你的什么?”
- - - - - - - - - - - - - - - - - - - - - - - - -
“他是我们的什么?”我眨了眨眼睛。
“我们的管理员,”珍妮向一个过来加入我们的警官挥挥手,“我们必须向他汇报。”
“你好,卡露莎博士,”警官对珍妮说,“这位是——?”珍妮点点头,作了一下介绍。“你们的衣服在里面,”警官说,他把我们带到里面。“按规定,你们在下面必须始终穿着它,头盔要戴好,即使是在是加过压的房间里。”
在方塔里面,这个有照明装置的地方原来是个中等大小的房间,墙上挂满了衣服,地面中央有个巨大的洞,周围被带网孔的栅栏所包围。“把你们的手放在那儿进行签到,”警官告诉我们,“接着去取衣服并检查一下。”当我们按其指示完成后,从洞里升起一个平台,停在与地面差不多高的地方。我们通过栅栏的门走进去,接着这个升降机就开始下降。
[关于作者]
Herb Sutter
是个独立顾问,也是ISO/ANSI C++标准委员会的秘书。你可通过hsutter@acm.org.联系他
Jim Hyslop
Leitch Technology International Inc.资深的软件设计师,你可通过jim.hyslop@leitch.com联系他