如何快速定位一个函数的返回点
如何快速定位一个函数的返回点,这对于一个比较短小精悍的函数来讲,从来就不是问题,但是假设我们有一个名为LongFunction的1000行长的函数, 调用如下:
bool bSuccess = LongFunction(); assert(bSuccess);
在运行中第二行弹出一个assert,我们知道肯定是LongFunction内部运行中出了什么问题导致其返回false。那么它内部出了什么问题,是在哪一行出错导致返回的?这恐怕不是一件容易的事,要知道这是一个1000行的函数,而且极有可能有很多的返回点。
我想这应该是我们日常工作中常见的问题,1000行的长函数在一些大型的系统中,老代码中应该还是不少见的。当然,有些朋友会强烈的认为这样的函数必须要重构,理由是~~~(此处略去500字)。的确,重构的好处是显而易见的,但很多时候由于资源,时间以及复杂度上的考虑,是不被采纳的。所以这里我们不考虑重构,只想找出一个能快速定位到函数返回点的方法。
我们先来分析一下可能的方案:
- 单步执行
这是最直接也是最浪费时间的方法,虽然你总能找到那个返回点,但显然,程序员是不会这么做的。 - 搜索并打断点
搜索函数中所有的"return"点,并在每一处设断点。这比第一点有效多了。虽然我们可以用正则表达式非常精确的定位到每一处真正的"return",但如果每次遇到这个情况都要搜一次,设一次,也比较麻烦。而且这种方法也有一点小瑕疵,假设以下代码:
if(a != b) return false;
我们会在这一行设上断点,执行时也会break进去,但这不一定是真正的返回点。
- 重定义return关键字
#define return TRACE(__LINE__); return;
如果我们的代码内建了这种机制,我只要看一下输出窗口的打印出的行号,就知道在哪里返回了。但是不能处理这个情况:if(bOK) return true;
- 自定义宏替换return
#defineRETURN(value) {TRACE(__LINE__); return value;}
的确,这是能工作的,但这要求我们修改所有现存的代码,更糟的是,以后编程也需要使用这个RETURN,让这个丑陋的RETURN成为guideline,我想大家都不愿意。
- 返回时构造
struct ReturnType{ ReturnType(bool){ } }; ReturnType LongFunction();
我们把LongFunction的返回类型改为ReturnType类,这个类的构造函数以返回类型为参数。这样,LongFunction返回时就会构造ReturnType,我们只要在其构造函数中设断点,在callstack中就能看到LongFunction是在哪里返回的。有的朋友可能会觉得这样对于不同的返回类型,就要写不同的构造函数,我们可以有两种方法来解决这个问题:
// 1. 变参 struct ReturnType{ ReturnType(...){ } }; // 2. 模板构造函数 struct ReturnType { template < typename T > ReturnType(T t){} };
这个方案的真正问题在于要求我们修改函数返回参数,这种接口的改动影响太大。
- 返回时析构
class ReturnMonitor { ~ReturnMonitor(){} }; bool LongFunction()
在资源管理中我们经常会用这种方式(RAII),现在我们利用函数返回时会调用析构函数这个特性,在析构函数中设断点,就能在callstack中看到返回点。这还有一个优点就是在LongFunction调用过程中如果出现异常,也能被捕捉。
{ ReturnMonitor mon; // Function body }
根据以上分析,我认为有两个方案只要稍加修饰,就能成为比较不错的候选方案:
第一个是"搜素并打断点",我们可以利用IDE的集成功能自动化这个步骤,比如说在Visual Studio中,我们可以写一个宏,或者写个插件来做这件事件,只要选中函数一点按钮,所有"return"自动打上断点。
第二个是"返回时析构",我们可以定义以下宏:
#ifdef _DEBUG #define RETURN_MONITOR ReturnMonitor mon; #else #define RETURN_MONITOR #endif
这样对于我们代码中比较长的,较难调试的长函数,就可以在函数开始加上RETURN_MONITOR,并且不影响release版本。