1

I'm trying to structure my code in a readable way. I've read that one way of doing it is as follows:

if(Init1() == TRUE)
{
    if(Init2() == TRUE)
    {
        if(Init3() == TRUE)
        {
            ...
            Free3();
        }
        Free2();
    }
    Free1();
}

I like this way of doing things, because it keeps each FreeX inside its matching InitX loop, but if the nesting goes beyond three levels it quickly becomes unreadable and goes way beyond 80 columns. Many functions can be broken up into multiple functions so that this doesn't happen, but it seems dumb to break up a function just to avoid too many levels of nesting. In particular, consider a function that does the initialization for a whole class, where that initialization requires ten or more function calls. That's ten or more levels of nesting.

I'm sure I'm overthinking this, but is there something fundamental I'm missing in the above? Can deep nesting be done in a readable way? Or else restructured somehow whilst keeping each FreeX inside its own InitX loop?

By the way, I realise that the above code can be compacted to if(Init1() && Init2()..., but the code is just an example. There would be other code between each InitX call that would prevent such a compaction.

Smurfs
  • 11
  • 2
  • 5
    You can't ask a coding style question and tag it both c++ and c (unless you're specifically contrasting them). Coding styles for those two languages are completely different. – David Schwartz Sep 30 '13 at 11:24
  • Your "by the way" does not work, because you wouldn't know which Init call failed. Therefore you wouldn't know which Free method(s) to call. – zennehoy Sep 30 '13 at 11:26
  • Erm...you'd just call them all -- that's what would happen in the nested ifs too. – Smurfs Sep 30 '13 at 11:34
  • Not if Init1 succeeds and Init2 fails. You wouldn't be calling Free1 then, which will leak resources. – zennehoy Sep 30 '13 at 11:35
  • I can't see that myself. If Init2 fails, Free1 gets called. Are you assuming that Init2 is going to throw an exception or something? – Smurfs Sep 30 '13 at 11:41
  • 1
    If you combine all Init methods into a single if statement, and the if statement fails (any of the Init functions returned false), there is no way to know which Init failed. Therefore there is no way to know which Free function(s) to call. – zennehoy Sep 30 '13 at 11:52
  • If this is C++, don't use TRUE, use built-in bool and you don't need to put == true on your if statements. – Neil Kirk Sep 30 '13 at 12:18

5 Answers5

4

Since you included the C++ tag, you should be using RAII - Resource Acquisition Is Initialization. There's a bunch of good online resources explaining this concept, and it will make a lot of things pertaining to resource management a lot easier.

zennehoy
  • 6,405
  • 28
  • 55
  • Thanks, but I'd prefer to not use exceptions. – Smurfs Sep 30 '13 at 11:26
  • RAII doesn't require you to use exceptions. It behaves nicely in the presence of exceptions, but doesn't require them (you can add checker methods, e.g., isValid, into the RAII classes to determine whether initialization succeeded without throwing exceptions). – zennehoy Sep 30 '13 at 11:28
  • It seems to me that writing classes to call a single function makes the code more complex, not more simple. – Smurfs Sep 30 '13 at 11:35
  • 5
    @Smurfs The Init function becomes the class's constructor, the Free function becomes the class's destructor, and you need about 4 lines of extra code to wrap a class around it. All in all, that's no more code than the original, since you no longer need the ifs. – zennehoy Sep 30 '13 at 11:41
  • 1
    @Smurfs In C++11, you can use `auto` with a lambda, which removes the only possible objection to using RAII. – James Kanze Sep 30 '13 at 12:13
  • @JamesKanze How do you create a lambda that is called on destruction? – zennehoy Sep 30 '13 at 12:16
  • @zennehoy Good question. I was probably thinking of some earlier proposals (where you could effectively make a lambda destructor). Otherwise: it's a bit tricky, but (and this works with g++): the lambda defines a block local class, with a destructor which does what you want, and you bind the `auto` to the results of calling the lambda (e.g. `auto cleanup = []() { ... }();`). Of course, you still have to define a complete class (with ctor and data) in the lambda, so I'm not sure what you gain over a normal local class. – James Kanze Sep 30 '13 at 14:02
  • @zennehoy But why didn't the committee create a variant of lambda where the "function" was treated as a destructor? I know that there was question of this at least once in the committee. – James Kanze Sep 30 '13 at 14:04
2

I'm sure I'm overthinking this, but is there something fundamental I'm missing in the above? [...] Or else restructured somehow whilst keeping each FreeX inside its own InitX loop?

Yes. This is a textbook case of code that will benefit enormously from RAII code:

instead of the construct:

if(init(3) == TRUE)
{
    free3();
}

consider this:

raii_resource3 r3 = init3(); // throws exception if init3 fails
                             // free3 called internally
                             // by raii_resource3::~raii_resource3

Your full code becomes:

raii_resource1 r1 = init1();
raii_resource2 r2 = init2();
raii_resource3 r3 = init3();

You will have no nested ifs, your code will be clear and straightforward (and focusing on the positive case).

You will just have to write RAII wrappers for resources 1, 2 and 3.

utnapistim
  • 26,809
  • 3
  • 46
  • 82
  • It seems to me that writing RAII wrappers makes the code more complex, not more simple. – Smurfs Sep 30 '13 at 11:35
  • 1
    They make client code simpler (and much safer) by extracting release of resources into separate code units. This adds a level of abstraction (better than before), extracts negative paths handling into separate code (better than before) and make client code simpler and straight forward for the positive case. It is more complex in levels of abstraction but simpler in client code complexity and maintainability. – utnapistim Sep 30 '13 at 11:42
  • 1
    Remember that RAII wrappers you are writing now may makes things more complex but you only write the code once and it may be used many, many times. – john Sep 30 '13 at 11:49
  • In many cases you can also write a generic (maybe templated) RAII wrapper and use it for various types of objects. In [this](http://stackoverflow.com/questions/2635882/raii-tutorial-for-c/2636897#2636897) answer I gave a practical example of a single class used for all kinds of WinAPI handle types. – utnapistim Sep 30 '13 at 11:53
2

As others have pointed out, the obvious answer is RAII. But if the issue of overly deep nesting comes up without RAII, you should really ask yourself if you aren't making your functions too complicated. A function should rarely have more than about ten lines (including such checks). If you look at real cases, you'll find that it almost always makes more sense to break the function up. Even with RAII, you should generally only have one instance of an RAII class per function. (There are exceptions, of course; arguably, something like std::lock_guard shouldn't count.)

James Kanze
  • 150,581
  • 18
  • 184
  • 329
0

What i would recommend would be to use a switch statement. I'm a big fan of switch statements when it comes down to code like this

Hope this helps.

switch (i) {
    case 1:
        // action 1
        break;
    case 2:
        // action 2
        break;
    case 3:
        // action 3
        break;
    default:
        // action 4
        break;
}
Mark Nicolle
  • 307
  • 1
  • 11
  • 2
    I'm new to this but I can't see how on earth the above switch statement is the equivalent of my if statements? – Smurfs Sep 30 '13 at 11:36
-1

You may have heard that gotos are evil, But if you are sticking to c, there is nothing "dirty" about using gotos to handle exceptions like this:

foo()
{
  if (!Init1())
    goto Error1;
  if (!Init2())
    goto Error2;
  ...
  ...
Error2:
  Free2();
Error1:
  Free1();
}
  • Nonsense. Every time anyone has tried to present a concrete example where `goto` made things clearer, it ends up that what was really needed was smaller functions. – James Kanze Sep 30 '13 at 11:53
  • @JamesKanze, this is clearly an opinion that one might have (I don't share it) but declaring other POV as "nonsense" is not appropriate here. – Jens Gustedt Sep 30 '13 at 12:03
  • 1
    So replace "nonsense" with bad software engineering. It's well established that this is bad practice, rapidly leading to incorrect and unmaintainable code. – James Kanze Sep 30 '13 at 12:12
  • Why is it bad software engineering? All the cleanup is in one place. It is basically a try catch/block, except the same cleanup block is used when there is no exception. (Which is not possible with try/catch.) I agree that raii is better, but you tell me how to do raii in c, and I'll give you an upvote :) – youdontneedtothankme Sep 30 '13 at 12:42