如何写优雅的代码(1)——灵活使用goto和__try:评论反馈

    //========================================================================
    //TITLE:
    //    如何写优雅的代码(1)——灵活使用goto和__try:评论反馈
    //AUTHOR:
    //    norains
    //DATE:
    //    Tuesday  21-July-2009
    //Environment:
    //    WINCE5.0 + VS2005
    //========================================================================
   
    本文会引起不少的争议,也算意料之中,毕竟文无定法。不过,有些朋友的观点我确实也考虑过,但并不是十分赞同。又因为直接回复格式实在无法统一,故在此撰文一并作答。
   
   
1.feelapi 发表于2009年7月16日 17:48:48  IP:举报
  VC8.0以后就不用担心SEH冲突的问题,都统一了,可以使用/EHa编译选项,就没问题了

  我用vs2005测试如下代码,还是不行:
 
  int WINAPI WinMain( HINSTANCE hInstance,
     HINSTANCE hPrevInstance,
     LPTSTR    lpCmdLine,
     int       nCmdShow)
  {   
   __try
   {
    std::vector<BYTE>::iterator iter;
   }
   __finally
   {}
 
   return 0;
  }
 
  我设置的方式如下:property-->c/c++--->Code Generation--->Enable C++ Exceptions,选择:yes with SEH exceptions(/EHa)。
 
  编译出错信息:Cannot use __try in functions that require object unwinding
 
  不知道是不是我设置的问题?
 

 
 
 
2.titilima 发表于2009年7月17日 11:41:01  IP:举报
do-while (0) 可以更优雅地解决这个问题,而且不会带来 SEH 的额外开销。

xylophone21 发表于2009年7月20日 12:55:49  IP:举报
do { break; }while(0)

jack1003 发表于2009年7月18日 9:45:02  IP:举报
探讨下:楼主这样实现也可以,但用TRY 总觉得有点费解,看看这样实现怎么样?
BOOL ReadDeviceBuf()
 {
 EnterCriticalSection(&g_csBuf);
 bool bOk = true;
 do { //打开驱动设备 HANDLE hDev = CreateFile(TEXT("DEV1:"), FILE_WRITE_ATTRIBUTES, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH, NULL);
 if(hDev == INVALID_HANDLE_VALUE)
 { bOk = false; break; } //获取驱动设备的缓存大小 DWORD dwSize = 0; if(DeviceIoControl(hDev,IOCTRL_BUFFER_SIZE,NULL,0,&dwSize,sizeof(dwSize),NULL,NULL) == FALSE)
  { bOk = false; break; } //分配缓存 g_pBuf = new BYTE[dwSize]; if(g_pBuf == NULL) { bOk = false; break; } //从驱动中读取数据 if(DeviceIoControl(hDev,IOCTRL_GET_BUFFER,NULL,0,&g_pBuf,dwSize,NULL,NULL) == FALSE)
  {
  delete []g_pBuf;
  g_pBuf = FALSE;
   bOk = false;
   break;
   }
   } while (false);
  
    CloseHandle(hDev);
    LeaveCriticalSection(&g_csBuf);
   
    return bOk ? TRUE:FALSE;
  }

 

    这样确实也可以,效率也比__try高,唯一的瑕疵是“词不达意”——毕竟while这关键字是用来表达循环的。这也是我为什么不选用while的原因,仅仅是个人喜好而已。
   
   
   
3.kinsan 发表于2009年7月17日 16:15:43  IP:举报
我面试人 只要有程序里写goto的 一律刷掉 goto 不容易掌握流程 程序规模越大就越难控制 就你那么几行代码 能说明什么? 世人鄙弃goto 不是没有理由的

看来微软wince团队的那些巨擘想进你们公司都不容易啊,因为wince的源代码中有太多的goto出自于这些牛人之手了...所以齐刷刷被你刷掉了... :-) 你可以查看一下wince5.0的public部分代码,一共有5086处用到了goto,这还没算private等其它的文件夹....

还有一些网友对此的看法:
kryso 发表于2009年7月17日 18:01:31  IP:举报
只能说你的教条主义严重.不容易掌握流程是代码编写能力的问题,证明你的水平没到家.

jzkdl 发表于2009年7月18日 15:00:29  IP:举报
我认为goto本身没有错~~ 对于goto来说更适合用在编写较底层的驱动代码~~或者简单函数里面~毕竟goto本身就是汇编JMP指令的代表~用goto有时候更容易理解计算机的运行原理~~ 试想一下,现在各种系统的内核不是还使用汇编吗~~ 如果你硬要拿goto和面向对象一起讨论~~我不发表意见~ 我个人非常支持作者的观点~~用goto只是为了优雅~~ 这里不是讨论到底应不应该用goto~~

wlzqi 发表于2009年7月18日 22:04:09  IP:举报
"我面试人 只要有程序里写goto的 一律刷掉 goto 不容易掌握流程 程序规模越大就越难控制 就你那么几行代码 能说明什么? 世人鄙弃goto 不是没有理由的" ---------你还太浅了。

ayave 发表于2009年7月18日 22:09:35  IP:举报
“我面试人 只要有程序里写goto的 一律刷掉 goto 不容易掌握流程 程序规模越大就越难控制 就你那么几行代码 能说明什么? 世人鄙弃goto 不是没有理由的” 哪个说的世人摒弃goto,linux内核满篇的goto, 我不认为linux内核就是垃圾。

cppmobile 发表于2009年7月19日 23:05:57  IP:举报
不需要盲目的去鄙弃一个东西,只要用得其所就可以用。

 

4.morphia 发表于2009年7月18日 7:07:55  IP:举报
为了个C 对象就改用goto!!非常糟糕! 真正的解决办法:把你的那些HANDLE用对象封装起来,像std::auto_ptr一样,然后还有你的与CriticalSection的东东都封装起来,在对象destroy的时候释放资源,使用_set_se_translator将SEH的异常封装成对象抛出,使用try catch处理各种不同类型的异常,才叫完美解决。 你这种解决办法,一两百行代码还行,代码多了你就完蛋了。

说实话,函数的目的就是精简。如果函数代码超过一两百行,再不分割为各自小函数的话,估计就是思路有问题了

 

5.morphia 发表于2009年7月18日 7:26:07  IP:举报
另外补充,try catch应该在该函数之外捕获异常,而当你的设备打开,读取,确定大小失败的时候就应该抛置你自己定义的异常,这时函数直接因为异常而退出,而因为你的HANDLE和CRITICALSECTION被对象封装,并且在对象释放的时候会自动释放HANDLE和LEAVE CRITICALSECTION,所以异常不会导致资源没有被释放。具体的,参见《More Effective C 》里有关章节。

cplusplus_zk 发表于2009年7月18日 13:54:29  IP:举报
俺认为,像里面那个临界区的处理,还是封装成一个类更优雅。用goto我可以容忍,但绝对不应该推荐,特别是很多C 并不是很精通的程序员很容易收到误导的。

barsteng 发表于2009年7月20日 17:13:49  IP:举报
同意morphia和cplusplus_zk 的观点,不知道楼主是否会考虑用类封装资源的管理更优雅?而且更容易被C developer理解我post了一个实现,可惜格式缩进什么的都被过滤了,呵呵


    用类来做互斥量的确是个好办法,比如:
class CLock
{
public:
 CLock(LPCRITICAL_SECTION pcsLock):
   m_pcsLock(pcsLock)
 {
  EnterCriticalSection(m_pcsLock);
 };

 virtual ~CLock()
 {
  LeaveCriticalSection(m_pcsLock);
 };
private:
 LPCRITICAL_SECTION m_pcsLock;

};

  调用的时候:
  BOOL ReadDeviceBuf()
 {
   CLock lock(&g_csBuf);
  
   ...
 }
 
    这在当前函数例子中表现的确很好,也在大部分情况下很优雅。但如果函数需要多次进入临界点,那么就不显得那么美好了,如:    
        
    static DWORD MonitorProc(LPVOID pParam)
    {
      CLock lock1(&g_csBuf);
      //做一些事情
      ~lock1(); //显式调用析构函数
     
      //做一些事情,比如通知别的进程数据有改变
     
      CLock lock2(&g_csBuf);
      //做一些事情
      ~lock2(); //显式调用析构函数
     
    }
   
    姑且不说调用析构函数和直接调用LeaveCriticalSection无异,就说显式调用析构函数,也属于极少使用的技法。这是这个方法的唯一瑕疵。当然,我举的这个反例或多或少是有点吹毛求疵了。
    
    
   
6. Analyst 发表于2009年7月20日 21:45:25  IP:举报
不管是用goto还是__try __finally都是糟糕的C 写法,只有C程序才会这么写。 正确的C 写法是这样的:
void ReadDeviceBuf()
{
 HANDLE hDev;
 try{
  CriticalSectionGuard lock((&g_csBuf);

  hDev = CreateFile(TEXT("DEV1:"), FILE_WRITE_ATTRIBUTES, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH, NULL);
  if(hDev == INVALID_HANDLE_VALUE)
   throw SysException(GetLastError());

  DWORD dwSize = 0;
  if(DeviceIoControl(hDev,IOCTRL_BUFFER_SIZE,NULL,0,&dwSize,sizeof(dwSize),NULL,NULL) == FALSE)
   throw SysException(GetLastError());

  //....

  CloseHandle(hDev);
 }
 catch(SysException& e)
 {
  CloseHandle(hDev);
  throw e;
 }
}

    这是一个看起来很美的写法,但实际上还是有问题的。
   
    写代码,有一个原则是:Input once,output once. 翻译成中文,也就是一进一出相对应。而这代码偏偏违反了这个原则。
   
    为了方便,我在代码加上标签:

void ReadDeviceBuf()
{
 HANDLE hDev;
 try{
  CriticalSectionGuard lock((&g_csBuf);

  hDev = CreateFile(TEXT("DEV1:"), FILE_WRITE_ATTRIBUTES, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH, NULL);
  if(hDev == INVALID_HANDLE_VALUE)
   throw SysException(GetLastError());

  DWORD dwSize = 0;
  if(DeviceIoControl(hDev,IOCTRL_BUFFER_SIZE,NULL,0,&dwSize,sizeof(dwSize),NULL,NULL) == FALSE)
   throw SysException(GetLastError());

  //....
A:
  CloseHandle(hDev);
 }
 catch(SysException& e)
 {
B:
  CloseHandle(hDev);
  throw e;
 }
}

    如标签A和B所示,这两处地方都属于同义资源释放。换句话说,这两部分的代码基本上是一模一样的,否则就达不到释放资源的目的。
   
    那么很显然,会出现这两个问题:
   
    1).如果释放资源的代码行数比较多,而A和B必须是同样的代码,由此造成函数的体积膨胀加快,显得极为臃肿。
   
      例如:
     
void ReadDeviceBuf()
{
 

  //....
A:
    delete [] pBuf1;
    delete [] pBuf2;
    NotifyError();
  CloseHandle(hDev);
 }
 catch(SysException& e)
 {
B:
    delete [] pBuf1;
    delete [] pBuf2;
    NotifyError();
  CloseHandle(hDev);
  throw e;
 }
}

    2).当代码持续膨胀,后续的修改者又不是作者本人的时候,很难保证标签A和B的代码一致。如果不一致,甚至只是少些一两段语句,那么肯定会造成资源泄漏。如果长时间运行,很有可能有异常发生。

 

    另外,更正一点,__try __finaly并不是什么糟糕的C写法,这个是SEH,由windows所特有支持的机制(所以不具备移植性)。至于__try __finaly是否是糟糕的做法,我只能说的是,在C#的语言层面已经支持和该机制类似的方法。对于号称去掉C/C++繁琐和难懂等弊端的现代C#语言,也支持该C++所不具备的特性,也许能说明一些问题吧?

posted @ 2009-07-21 14:52  我的一天  阅读(168)  评论(0编辑  收藏  举报