GotW#63 狂乱的代码

原文参见:www.gotw.ca/gotw/063.htm

难度:4/10

有时生活中你会遇到一些看似平常却不可思议的调试情形。继续尝试解决这个问题,看看你能否解释可能导致问题的原因。

问题:

1.一个程序员写下以下代码:

//--- file biology.h

//

// ... 适当的包含文件和其它材料 ...

class Animal

{

public:

//基于该类对象的方法:

//

virtual int Eat    ( int ) { /*...*/ }

virtual int Excrete( int ) { /*...*/ }

virtual int Sleep  ( int ) { /*...*/ }

virtual int Wake   ( int ) { /*...*/ }

// 对于已经有配偶的动物,

// 有时它们不喜欢它们的配偶,我们提供了:

int EatEx    ( Animal* a ) { /*...*/ };

int ExcreteEx( Animal* a ) { /*...*/ };

int SleepEx  ( Animal* a ) { /*...*/ };

int WakeEx   ( Animal* a ) { /*...*/ };

// ...

};

// 具体类。

//

class Cat    : public Animal { /*...*/ };

class Dog    : public Animal { /*...*/ };

class Weevil : public Animal { /*...*/ };

// ... 更多可爱的生物 ...

// 虽然冗余却很方便的外覆(helper)函数。

//

int Eat    ( Animal* a ) { return a->Eat( 1 ); }

int Excrete( Animal* a ) { return a->Excrete( 1 ); }

int Sleep  ( Animal* a ) { return a->Sleep( 1 ); }

int Wake   ( Animal* a ) { return a->Wake( 1 ); }

不幸的是,代码无法通过编译。编译器拒绝了一个或更多的...Ex()函数的定义,并给出出错信息表明函数已经被定义了。

为了绕开编译器错误,这个程序员将...Ex()函数注释掉,使程序通过了编译,并开始测试sleeping函数。很不幸,Animal::Sleep()成员函数看来没有一直正确地运行;当他试图直接调用Animal::Sleep()时,一切正常。但当他试图通过Sleep()这个作为包裹的自由函数来调用Animal::Sleep()时——事实上这个包裹函数除了调用成员函数版本的Sleep()外什么也没做——有时却什么都不做...并非每次都如此,只是某些时候才发生这种情况。最后,当程序员在调试器中或是进入到连接器生成的符号表中,以试图找出症结所在时,他发现根本找不到Animal::Sleep()的代码。

编译器出毛病了吗?是不是这个程序员应该给编译器的提供商发一封令人光火的电邮并且向纽约时报提交一封愤怒的信呢?难道是千年虫?或者只是因为从因特网上感染了淘气的病毒呢?

究竟是怎么了??

解答:

简短地说,有几种情况可能导致以上症状,但有一种显著的可能情况能够解释所有观察到的行为。对,你猜对了,正是狂乱运作的宏和有意无意的重载在作怪。

<动机>

某些流行的C++编程环境为你提供了更改函数名的宏。通常它们出于“良好的”或“清白的”原因在工作,即是为了向前和向后的API版本的兼容;例如:如果一个Sleep()函数在某个版本的操作系统中被替换成SleepEx(),那么供应商所提供的声明函数的头文件中就可能包含自动将Sleep转换成SleepEx的这么一个“有用的”宏。

这不是个好主意。宏是封装的敌人,因为无法控制它们的实际作用范围,即使是宏的作者也无能为力。

<宏所忽略了的>

因为某些原因,宏被视为“令人讨厌的、恶臭的、乱拱被子的同床者”(obnoxious, smelly, sheet-hogging bedfellows),而绝大部分原因都是跟它作为一种“久负盛名”的文本替换工具的本质相关的。宏在预处理期即开始起作用,而此时任何C++的语法和语义规则都还没有被应用。以下就是宏的一些缺乏吸引力的特性:

1.宏会更名——更多时候会伤害无辜者而不是保护它们。

保守一点说,这种重命名会在一定程度上扰乱除错工作的进行。这种宏的重命名工作意味着你的函数事实上并不如你所愿的被调用。

举个例子,考虑我们的非成员函数Sleep():

int Sleep ( Animal* a) { return a->Sleep(1); }

你无法在目标代码或链接表文件的任何地方找到Sleep(),因为里面根本没有Sleep()函数。起初,当你想知道你的Sleep()去向时,你也许会觉得,“啊,或许编译器自动帮我将Sleep()内联了(..inlining Sleep() for me.)。”因为那样可以解释为什么简短的函数看上去不存在——虽然很显然编译器不会在没有直接指明的情况下内联任何东西。你立即得出了结论,于是火冒三丈地给你的编译器供应商去了一封气愤的电邮以抱怨过分的(agressive)优化,然而,你错怪了这家公司(或者,至少,错怪这部门)。

你们中的一些人也许已经遇到过不幸的、的确很糟糕的结果了。如果你们像我一样,容易被编译器的古怪所激怒,并且不满足于简单的解释,那么你强烈的好奇心也许能打败你:)接下来,你也许会打开调试器,故意单步跟踪进入函数...只是被带入正确的幻影函数所在的源代码行(在源代码中,它看上去还是原先的名字),继续单步跟踪进那个幻影函数,你会发现它的确在工作、在被调用,然而所有其它调用它的用户却都不知道它的存在。通常,在“找出究竟该怎么办”与“只是低声抱怨愚蠢的宏的诡计”之间只差一小步。

等等,情况变好了:

1(b). C++已经具备解决名字问题的特性了。这导致了可能的“不健康的交互作用”。你也许认为改变函数名字不是什么大问题。好,很好,通常的确如此。但是如果你改变了函数的名字结果它与另一个已经存在的函数重名了...如果两个函数重名了,C++该怎么办呢?它会重载它们。如果你没有意识到悄悄发生的重载的话,那可就不太好了。啊,这就正如Sleep()案例一样。原因就在于库的供应商提供了一个“有用的”Sleep宏,用以自动将函数重命名为SleepEx,从而导致事实上在供应商的库中有重名的函数。如果不同的函数有不同的签名呢?那么我们在写我们自己的Sleep()函数时,我们就可以发觉库中的Sleep()的重载,从而小心地避免不确定性和其它一些导致问题的错误。我们甚至还得依赖于重载,因为我们也许会刻意要提供与库中Sleep()类似的行为。换句话说,一旦我们的函数被悄悄地重载后,不仅仅是被重载的函数行为异于我们所期待的,而且如果我们原本刻意设计了重载,那么它也将什么都不做,至少它不按照我们想的那样去运作。

在我们问题的上下文中,这么个重命名宏只能部分地解释为什么在不同环境下最终调用的是完全不同的函数。哪一个函数被调用依赖于重载决议在不同的调用点所决定的特定类型。有时是我们的函数,有时是库函数。仅仅是依赖,也许以一种不明显的方式。

如果这个故事以令人不快的对于非成员函数的影响结束的话,这远远不够。很不幸,一些危险的东西就像榴霰弹(shrapnel)一样向其它方向飞去:

2.宏忽略了类型

上面描述的Sleep宏的本来意图是改变一个全局非成员函数的名字。然而,这个宏改变了所有出现Sleep的地方;如果我们恰好有一个全局变量Sleep,那么它的名字也会被悄悄的改掉。这绝对是件糟糕的事情。

3.宏忽略域控制范围

更糟糕的是,一个改变全局非成员函数的宏会很乐意地改变所有匹配的函数(或其它东西)的名字,也许这些函数是类的成员或已经被很好地封装在你自己的名字空间中了。在这个例子中,我们写下了包含Sleep()和SleepEx()的类;上面许多问题都与Sleep更名宏有关,至少部分有关。这个宏使我们自己的函数互相隐式地重载了。正是这种看不见的重载,解释了上面第一点中提到的为什么有时候非期望的成员函数被调用。这绝对是个坏东西。这就像一个脱下手套的、无证医生(不够聪明的库的头文件作者)用脏手(“未经消毒的”宏)剖开你的身体(类或名字空间),然后在你的体腔里重新排列东西(类成员和其它代码)一样...而这一切竟然是他在梦游中完成的(完全没有意识到他们在做什么)。

<概要>

简而言之,宏对任何东西都满不在乎:)

策略:避免宏。

你对宏默认的反应应该像这样,“宏!呃,真讨厌!”。除非在一些特殊情况下被迫要使用它们,并且要保证它们不搞破坏。宏不是类型安全(type-safe)的,也不是域安全(scope-safe)的...它们根本不安全。如果你必须写宏,应该避免它们出现在头文件中,并且要试图赋予它们足够长并且个性化的名字,这样才不至于无意中伤害到别的东西。

策略:尽量使用名字空间。

确实,像上面看到的一样,宏并不尊重域,当然也包括名字空间域。然而,将问题中的自由函数放进名字空间也许至少会减少一些问题。有一点可以明确,这样可以避免被供应商提供的库函数全局重载。简单的说,要施行优良的封装结构。好的封装不仅仅有利于更好的设计,还可以帮助你对抗未预见到的威胁。宏是封装的敌人,因为无法控制它们的实际作用范围,即使是宏的作者也无能为力。类和名字空间是C++中管理并使程序各不应相关部分之间耦合最小化的极其有用的工具。明智地使用它们和其它C++工具来加强封装将不仅仅是有利于做出出众的设计,更将提供一种针对其它程序员所作的欠周全的代码的保护措施。

GotW#63 狂乱的代码相关推荐

  1. [转]GotW#63 狂乱的代码

    GotW#63 狂乱的代码 原文参见:www.gotw.ca/gotw/063.htm [转]Xuan Xie. 难度:4/10 有时生活中你会遇到一些看似平常却不可思议的调试情形.继续尝试解决这个问 ...

  2. 请善用工具审核您的内核代码:)

    在写内核代码时.代码风格(coding style)是一个非常重要的部分,否则内核代码将变的混乱不堪. 那么什么样的代码算美丽的代码?什么样的代码符合c99这种标准?此外,程序写完之后,有什么工具可以 ...

  3. BUPT-CSAPP 2019 Fall 3.58 3.60 3.63

    深入理解计算机系统 写在前面: 1.这是我的个人作业,在这里写什么样我交上去就是什么样,希望各位不要原样抄袭. 2.这里的题目只是我OCR了方便搜索,真要看题目还是得书. 3.CSDN的Markdow ...

  4. 北邮22信通:二叉树的遍历书上代码完整版

    北邮22信通一枚~    跟随课程进度每周更新数据结构与算法的代码和文章  持续关注作者  解锁更多邮苑信通专属代码~ 上一篇文章: 下一篇文章: 目录 一.储存最简单数据类型的二叉树 代码部分: 代 ...

  5. Socket IO与NIO(二)

    UDP: 英文:User Datagram Protocal,缩写为UDP. 是一种用户数据报协议,又叫用户数据报文协议. 是一个简单的面向数据报的传输层协议,正式规范为RFC 768. 用户数据协议 ...

  6. Android:学习AIDL,这一篇文章就够了(上)

    前言 在决定用这个标题之前甚是忐忑,主要是担心自己对AIDL的理解不够深入,到时候大家看了之后说--你这是什么玩意儿,就这么点东西就敢说够了?简直是坐井观天不知所谓--那样就很尴尬了.不过又转念一想, ...

  7. (写给像我一样刚离开校园进入公司的小菜鸟)在领域架构下,如何实现简单的展示页面以及增删改查(第二步)...

    这一片就简单的介绍实现增删改查 首先是显示所有数据 Service=>>> 1 /// <summary> 2 /// 加载所有数据 3 /// </summary ...

  8. Android中级篇之百度地图SDK v3.5.0-一步一步带你仿各大主流APP地图定位移动选址功能

    from: http://blog.csdn.net/y1scp/article/details/49095729 定位+移动选址 百学须先立志-学前须知: 我们经常在各大主流APP上要求被写上地址, ...

  9. Learn python the seventh day

    # 破解极验滑动验证 '''破解极验滑动验证破解极验滑动验证博客园登录url: https://account.cnblogs.com/signin?returnUrl=https%3A%2F%2Fw ...

最新文章

  1. mysql 隐式失误_评“MySQL 隐式转换引起的执行结果错误”
  2. ORA-00910: specified length too long for its datatype
  3. xms和xmx为什么要相同_股民为什么要做股票配资?
  4. 有道云笔记的word文档导入功能
  5. 电饼锅的样式图片价格_进口珐琅铸铁锅专场,精致小厨娘们来康康!
  6. python使用opencv_教你快速使用OpenCV/Python/dlib進行眨眼檢測識別!
  7. android消息提示方法自定义,Android自定义消息提示容器
  8. 各种字符串Hash函数
  9. 我帮公司财务写了个“群发工资条”的 Python 脚本!
  10. openairinterface 中手动安装编译 UHD, Ubuntu 16.04
  11. 购买课程赠老男孩出版的签名新书啦!
  12. android 函数式编程,响应式编程在Android中的应用
  13. Linux - Ubuntu Server基础
  14. MAC中安装Navicat Premium
  15. matlab矩阵最大值最小值均值,Matlab 处理数据—最小值、最大值、均值、方差
  16. PHP接入谷歌验证器(Google Authenticator)
  17. 从头到尾跑起来一个SpringBoot系统
  18. 什么值传递和引用传递
  19. MVC中方便的[Authorize],加上这特性,就可以加上登录验证
  20. 基于CSS盒模型的页面布局

热门文章

  1. C# 获得本机IP、端口等信息地址以及服务器IP信息
  2. 如何用Windows命令提示符(cmd.exe)进入指定目录
  3. 因特网在线聊天协议(IRCP/IRC)--网络大典
  4. freemarker如何获取当前时间或者时间戳?
  5. VBA遍历文件夹下的所有文件
  6. Windows Server 2008 R2部署active directory服务器
  7. DRN: A Deep Reinforcement Learning Framework for News Recommendation理解
  8. JavaScript Errors 指南
  9. 布法罗大学计算机硕士学费,美国水牛城大学学费贵不贵(美国水牛城大学往年排名情况怎么样)...
  10. 支付宝转账到银行卡的二维码