Share via


Designing code for debugging

I would like to advocate designing code so that it is easier to debug. When write code, we thinking about many different things:

  1. What edge cases am I missing?
  2. How will this code perform?
  3. How readable is this code?
  4. etc.

But most programmers, even really good programmers, don’t think debugging while writing their code.

Here are a few tips:

  1. Use lots of explaining variables (https://c2.com/cgi/wiki?IntroduceExplainingVariable). You can easily change the value of the explaining variable under the debugger. You can inspect the value of the variable before it goes into the next function. 

  2. Don’t call multiple non-trivial functions from the same statement. If I have:

    Func1(Func2(Func3()));

    It becomes a pain in the butt to pick which function to step into. It is also difficult to inspect the return value, and change it if necessary.

  3. Don’t use IfFailGo(Func()). IfFailGo is fine, but divide it up into two statements:

    hr = Func();
    IfFailGo(hr);

  4. Prefer inline functions to macros. The C++ compiler doesn’t include macros in the PDBs, but inline functions are still there, and you can step into them or func eval them.

  5. Prefer enums to #defines or GUIDs. The debugger can give you the human comprehendible version of enums, but never #defines, and only sometimes GUIDs. Clearly sometimes you don’t have a choice, but if you do, enums are great.

  6. Use actual types instead of VOID*/cast

Comments

  • Anonymous
    June 07, 2004
  1. Use actual types instead of VOID*/cast
    Please forward to the team that wrote COM.
  • Anonymous
    June 07, 2004
    Nice Gregg but I have one personal take on tip #3. I think it should read:

    3 Don't use IfFailGo(exp) or anything like it.

    In case some of you aren't aware of IfFailGo(exp) it is a macro (hey, what about tip #4?) that many developers around Microsoft use as an easy method to change the flow of a function based on an error HRESULT.

    In one implementation it's declared as:

    #define IfFailGo(EXPR)
    do { hr = (EXPR); if(FAILEDHR(hr)) goto Error; } while (0)

    Personally, I find this method of short circuiting a function quite cluttering and makes it far too easy to forget to cleanup correctly. Since many, many times I've seen :Error labels used implicitly as :Success labels. OR, the goto simply skips over some cleanup that is required.

    What do I do instead?
    1) Use a few levels of nested if's
    2) Split a deeply nested if (say over 3 levels) into a separate function).
    3) Introduce explaining variables and test on those.
    4) Artificially pop out of a deeply nested if and start from "the top".

    Not only does the avoidance of IfFailGo allow you to better declare variables closer to where they are used (Just try to declare a new auto after an IfFailGo, without introducing another set of brackets.), it also has helped me follow function flow while stepping through under the debugger. If while blindly stepping, you suddenly get an error, you're immediately jettisoned to the :Error label, leaving you trying to guess what particular IfFailGo just fired.
  • Anonymous
    June 07, 2004
    The comment has been removed
  • Anonymous
    June 07, 2004
    Krzysztof Kowalczyk weblog
  • Anonymous
    June 08, 2004
    Ash,
    I oversimplified the macro as it is really #defined in the code-base I was looking at. Indeed there are macros in that code-base that allow the user to provide the label, rather than referring to a constant :Error. But, most users just simply fall back to the one that DOES hardcode the label. Programmers are a lazy bunch, aren't they?

    But, even with that improvement, I still think it's a practice that only gets better by being avoided. :)