首页
社区
课程
招聘
[翻译]关于C++编程的42条建议(八)
发表于: 2017-3-10 19:35 3714

[翻译]关于C++编程的42条建议(八)

2017-3-10 19:35
3714



35. 在枚举中加了新的枚举常量后别忘了修改switch运算


下面的代码来自Appleseed项目。代码中包含的错误被PVS-Studio诊断为:V719 switch语句没有覆盖枚举InputFormat”的所有值,少了InputFormatEntity.


enum InputFormat
{
    InputFormatScalar,
    InputFormatSpectralReflectance,
    InputFormatSpectralIlluminance,
    InputFormatSpectralReflectanceWithAlpha,
    InputFormatSpectralIlluminanceWithAlpha,
    InputFormatEntity
};

switch (m_format)
{
  case InputFormatScalar:
    ....
  case InputFormatSpectralReflectance:
  case InputFormatSpectralIlluminance:
    ....
  case InputFormatSpectralReflectanceWithAlpha:
  case InputFormatSpectralIlluminanceWithAlpha:
    ....
}


解释


有的时候我们需要在已经存在的枚举里加入新的元素,当我们做这个操作的时候,我们需要特别的谨慎——因为我们要检查全部代码看看哪里有用到这个枚举,比如说在switchif中。上面给出的代码就是这种情况。


InputFormat中加了InputFormatEntity——这里是我想象的,因为确实在InputFormat的后面有加入这个常量。很多时候,程序员在枚举后面加了新的常量,然后忘了检查代码以确保他们有正确处理这个常量,也没有修改switch操作。


最后的结果就是,在这个例子中,并没有处理到"m_format==InputFormatEntity"的情况。


正确代码


switch (m_format)
{
  case InputFormatScalar:
  ....
  case InputFormatSpectralReflectance:
  case InputFormatSpectralIlluminance:
  ....
  case InputFormatSpectralReflectanceWithAlpha:
  case InputFormatSpectralIlluminanceWithAlpha:
  ....
  case InputFormatEntity:
  ....
}


建议


让我们想想,怎样在代码重构的时候避免这种错误?最简单,但不那么有效的解决方法就是加一个default’,它可以输出一个信息,像这样:


switch (m_format)
{
  case InputFormatScalar:
  ....
  ....
  default:
    assert(false);
    throw "Not all variants are considered"
}


现在,如果变量m_formatInputFormatEntity,我们就可以看到一个异常。这样的处理方法有两个不好的地方:


  1. 因为有可能在测试阶段这个错误并没有显现出来(如果在测试的时候,m_format不等于InputFormatEntity),那这个错误就会流入发布版本,以后才会显现——在客户运行时出现。如果要顾客来反映这种问题,真的很糟糕。

  2. 如果我们把default考虑为一个错误,那我们就不得不写一个case来解决枚举所有可能的值。这样就很不方便,尤其是当一个枚举中有很多常量的时候。有时,用default来处理不同case真的很方便。

我建议用以下的方法来解决这个问题,我不敢说这个方法很完美,但至少它有解决到问题。


当你定义一个枚举的时候,要确保你也加了一条特殊的注释。你也可以用关键词和枚举名。


例子:

enum InputFormat
{
  InputFormatScalar,
  ....
  InputFormatEntity
  //If you want to add a new constant, find all ENUM:InputFormat.
};

switch (m_format) //ENUM:InputFormat
{
  ....
}


在上面的代码中,当你要改变枚举InputFormat,你就可以直接在项目的源代码中查找ENUM:InputFormat”


如果你是在一个开发者团队中,你可以告诉你的小伙伴们这个约定,然后把它加入到你们的编程标准和风格指引中。如果有人没能遵守这条原则,真遗憾。


36. 如果你的PC发生了什么奇怪的事,检查一下内存


我想在看了枚举类型的错误后,你肯定很累了。所以这次,让我们休息一会,不看代码了。


一个典型的场景——你们的项目没有正确运行。但你也不知道发生了什么。在这种情况下,我建议你不要急于去责备某个人,而应该把焦点放到你们的代码上。在99.9%的案例中,罪恶之源就是你们的开发团队中某个人引入了bug。通常这个bug都非常的愚蠢且平淡无奇。所以快点花点时间去找找。


实际上,这个一直出现的bug也不能说明什么。你可能只是遭遇了海森堡bugHeisenbug.


责备编译器也不是什么好主意。它当然也会出错啊,虽然真的非常少。比如说,你会发现就是仅仅错用了sizeof(),这件事就会变得很棘手。我的博客里有篇博文就是关于它的:The compiler is to blame for everything


但为了让记录更公正一点,我应该说,还是会有例外存在的。很少的情况下,bug不对代码做什么。但我们还是应该注意到这种可能性的存在。这可以帮助我们保持理智。


我会用一个我曾经遭遇过的例子来证明这种可能性。非常幸运,我当时有截图。


当时,我在做一个简单的测试项目,想要去演示Viva64分析器(PVS-Studio的前身)的功能,但是它没能正确运行。


经过漫长而令人讨厌的调查之后,我发现是一个内存槽引起这所有的问题。更确切地说,是1bit。你可以看看下面的照片,我当时正在调试,想往这个存储单元写入3’




在改变内存之后,编译器开始读值并将其显示在窗口上,但它显示的是2:看,地址是0x02。尽管我已经把值设置为3了。低位总为0.



一个内存测试项目证实了这个问题。非常奇怪,这台电脑一直都工作得好好的,没出什么问题。最后为了让项目正确运行,我换了内存条。


我真的很幸运。当时我处理的是一个简单的测试项目。虽然我还是花了很多时间去了解发生了什么。我花了两个多小时去检查汇编器目录,想要找到引起这种奇怪行为的原因是什么。是的,我当时有因此责备编译器。


我无法想象,如果是一个实际的项目,我要花多大的时间精力去解决这个问题。谢天谢地,我当时不用去调试其他的东西。


建议


要时刻检查你代码中的错误。别想着推卸责任。


但是,如果一个bug只在你的电脑上出现,而且都快一周了。那么,可能不是因为你的代码。

要持续寻找bug。但是在回家前,可以运行一个一整夜的RAM测试。或许,这个简单的测试步骤能节省你的时间精力。


37. 要小心在do {...} while (...)里面的‘continue’操作


下面的代码来自Haiku项目(BeOS的继承者)。代码中包含的错误被PVS-Studio诊断为:V696. 'continue'操作会终止'do { ... } while (FALSE)' 循环,因为判断条件一直是false


do {
  ....
  if (appType.InitCheck() == B_OK
    && appType.GetAppHint(&hintRef) == B_OK
    && appRef == hintRef)
  {
    appType.SetAppHint(NULL);
    // try again
    continue;
  }
  ....
} while (false);


解释


do-while循环里的continue的运作机制可能跟某些程序员想象的不一样。当遇到continue的时候,会先要检查循环的终止条件。我想要更详细的解释这个问题。假设有个程序员写了这么一段代码:


for (int i = 0; i < n; i++)
{
  if (blabla(i))
    continue;
  foo();
}


或者是这样的:


while (i < n)
{
  if (blabla(i++))
    continue;
  foo();
}


很多程序员凭直觉理解,当遇到continue,就要(重新)评估控制条件(i<n,然后只有为真的时候才会进入下一个循环迭代。但,如果程序员的代码是这么写的:


do
{
  if (blabla(i++))
    continue;
  foo();
} while (i < n);


直觉就不起作用了哦。因为他们没有在continue上面看到判断条件,而且,似乎对于他们来说,continue会马上触发下一个循环迭代。事情并非如此,continue还是像它平常所做的那样——先重新评估控制条件。


除非踩了狗屎运,不然缺乏对continue的理解真的很难不出错。无论如何,如果循环条件 一直是false,这个错误肯定会发生。因为在上面给出的代码中,程序员打算在之后的循环中执行特定的操作。代码中的注释//try again ”证明了他们有这个意图。当然不会有again’啦,因为判断条件一直是false,而且一遇到continue,循环就会终止哦。


换句话说,在这个do {...} while (false)结构中,continue就相当于break


正确代码


要写正确代码有很多选项。比如,创造一个无限循环,然后用continue继续,用break退出。


for (;;) {
  ....
  if (appType.InitCheck() == B_OK
    && appType.GetAppHint(&hintRef) == B_OK
    && appRef == hintRef)
  {
    appType.SetAppHint(NULL);
    // try again
    continue;
  }
  ....
  break;
};


建议


尝试着不要在do { ... } while (...)continue。即使你真的知道这是如何运作的。因为一不小心你就会犯这种错误,而且/或者你的同时就不能正确理解代码啦,最后把它错改。我一直都这么说:一个优秀的程序员不是知道并使用其他语言技巧的那一个,而是能够写出干净易懂的,即使新手也能理解的代码的那一个。


38. 从现在开始,用nullptr不要用NULL


新的C++标准引入很多有用的改变。有些我现在还没有用到,但是有些应该马上用起来,因为用它们有诸多益处。


其中一个现代化标准就是关键字nullptr, 其旨在代替NULL宏。


让我提醒你一下,在C++中,NULL的定义是0 ,没有其他的了。


当然,看起来它好像就是一些语法糖(syntactic sugar)。那么当我们写nullptr或者NULL的时候,其中的区别是什么呢?真的有区别!用nullptr可以帮我们避免大量的错误。我将会用一些例子来证明。


假设有两个重载函数:


void Foo(int x, int y, const char *name);
void Foo(int x, int y, int ResourceID);


一个程序员可能会这么写函数调用:


Foo(1, 2, NULL);


然后那个程序员坚信自己这么做是在调用第一个函数。但因为NULL0而非其他东西,然后0又是整形,所以其实调用的是第二个函数。


然而,如果程序员用的是nullptr,就不会出现这样的问题,而且第一个函数也能正确调用。另一种比较常见的使用NULL的例子是这样的:


if (unknownError)
  throw NULL;


在我看来,传一个指针到异常里面真的很奇怪。尽管如此,还是有人这么做。这样看来,那些程序员应该是这么写代码的。无论如何,关于这样写是好是坏的讨论已经超纲了。


最重要的是,那个程序员打算在处理未知错误的时候抛出一个异常,然后‘发送’一个空指针到外部世界。


事实上,这不是一个指针,而是一个整形。结果就是,异常处理的结果不像程序员所期望的那样。


"throw nullptr;"这行代码能让我们避免不幸,但是并不意味着我相信这样的代码能被接受。


在一些例子中,如果你用nullptr,不正确的代码就不会被编译。


假设有些WinApi函数返回一个HRESULT类型。HRESULT类型没有能用来处理指针的东西。然而还是有可能会写出类似这种无意义的代码:


if (WinApiFoo(a, b, c) != NULL)


这行代码能够编译,因为NULL0,是一个整形,然后HRESULT是一个长整形。长整形和整形是可以比较值的。如果你有nullptr,那下面的代码就不会编译。


if (WinApiFoo(a, b, c) != nullptr)


因为编译错误,程序员就能够注意到并修改这行代码。


我想你已经明白我的意思了。有很多这样的例子。但大部分是人为的例子。而有的时候这种例子不那么有说服力。所以,有真实的例子吗?有的。这是其中一个。唯一的问题——它不是那么好看或者简短。


这个代码是来自MTASA项目。


所以,有RtlFillMemory()哈。这可以是一个真实的函数或者一个宏。没关系。类似于memset()函数,但第二第三个参数互换了位置。我们先来看一下这个宏是如何声明的:


#define RtlFillMemory(Destination,Length,Fill) \
  memset((Destination),(Fill),(Length))


还有FillMemory(),它跟RtlFillMemory()没什么区别:


#define FillMemory RtlFillMemory


是,一切都很长很复杂。但至少这是一个真实案例中的错误代码。

这里还有个用到了FillMemory宏的代码。


LPCTSTR __stdcall GetFaultReason ( EXCEPTION_POINTERS * pExPtrs )
{
  ....
  PIMAGEHLP_SYMBOL pSym = (PIMAGEHLP_SYMBOL)&g_stSymbol ;
  FillMemory ( pSym , NULL , SYM_BUFF_SIZE ) ;
  ....
}


这段代码有很多bug。我们可以清晰的看到这里至少2到3个参数很混乱。这就是为啥分析器给出了两个警告 V575:


V575 'memset' 函数要处理”512“值。检查第二个参数。crashhandler.cpp 499

V575  'memset' 函数要处理”0“元素。检查第三个参数。crashhandler.cpp 499


代码可以编译是因为NULL0. 结果就是0数组元素被填充。但实际上,问题不仅仅是这个。一般来说,NULL出现在这里是不合适的。memset()函数按字节处理,所以让内存充满NULL值没什么意义。这有点荒谬。正确的代码应该是这样:


FillMemory(pSym, SYM_BUFF_SIZE, 0);


或者这样:


ZeroMemory(pSym, SYM_BUFF_SIZE);


但还不是重点,重点是,这段没什么意义的代码会编译成功。然而,如果一个程序员已经养成了用nullptr不是用NULL的习惯,那他应该就会这么写:


FillMemory(pSym, nullptr, SYM_BUFF_SIZE);


这样的话,编译器就会给出一个错误信息,然后程序员就会意识到他们可能哪里出错了,然后就会更加注意他们编程的方式。

注意。我知道,在这个例子中,我们不能把一切都归咎于NULL。但是也因为NULL,不正确的代码成功编译了,没有输出任何警告。


建议


开始使用nullptr。从此刻开始。还有,在你的公司的编程标准中做必要的修改。

nullptr会帮助你避免某些愚蠢的错误,然后就可以稍微加快开发进程。


原文链接:https://software.intel.com/en-us/articles/the-ultimate-question-of-programming-refactoring-and-everything

本文由 看雪翻译小组 lumou 编译


第一部分:http://bbs.pediy.com/thread-215956.htm

第二部分:http://bbs.pediy.com/thread-216074.htm

第三部分:

第四部分:

第五部分:http://bbs.pediy.com/thread-216146.htm

第六部分:http://bbs.pediy.com/thread-216171.htm

第七部分:http://bbs.pediy.com/thread-216233.htm


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
免费 1
支持
分享
最新回复 (3)
雪    币: 44229
活跃值: (19955)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
2
辛苦了
2017-3-10 19:43
0
雪    币: 2305
活跃值: (4554)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
可不可以 都放在一个pdf中·!~
2017-3-10 21:38
0
雪    币: 34
活跃值: (864)
能力值: ( LV12,RANK:380 )
在线值:
发帖
回帖
粉丝
4
值得怀疑 可不可以 都放在一个pdf中·!~
下午把所有的翻译好之后就集中成一个。
2017-3-11 08:21
0
游客
登录 | 注册 方可回帖
返回
//