12

Often I built functions, in C, that checks some parameters and return an error code.

Which is the best approach to stop the values checking when I found an error?

First example:

ErrorCode_e myCheckFunction( some params )
{
  ErrorCode_e error = CHECK_FAILED;

  if( foo == bar )
  {
     if( foo_1 == bar_1 )
     {
        if( foo_2 == bar_2 )
        {
           error = CHECK_SUCCESS;
        }
     }
  }

  return error;
}

Second Example:

ErrorCode_e myCheckFunction( some params )
{
  if( foo != bar )
  {
     return CHECK_FAILED;
  }

  if( foo_1 != bar_1 )
  {
     return CHECK_FAILED;
  }

  if( foo_2 != bar_2 )
  {
     return CHECK_SUCCESS;
  }
}

I prefer the first approach because I read that the MISRA rules avoid multiple return statement.

Which is the best approach?

Melebius
  • 6,183
  • 4
  • 39
  • 52
Federico
  • 1,117
  • 6
  • 20
  • 37
  • 1
    if you prefer the first one, that's the best one (for you) :) – Chris Turner Mar 03 '17 at 10:04
  • This is actually not an opinion-based question. The OP prefers one version because MISRA-C tells him to, with no rationale stated. The real question here is why MISRA-C makes such a statement. In the past, I've gone to the bottom of why this rule even exists, answer below. – Lundin Mar 03 '17 at 10:58
  • Possible duplicate of [Pattern to prevent continually checking error?](http://stackoverflow.com/questions/24658258/pattern-to-prevent-continually-checking-error) – Toby Mar 03 '17 at 11:16
  • @Toby Not a duplicate because it doesn't address the MISRA aspect. – Lundin Mar 03 '17 at 11:45
  • @Lundin doesn't _explicitly_ mention MISRA, but in the question I express preference for a single exit point (IIRC it was because of the MISRA req anyway) and the answer provides examples that all speak to that. WRT actually answering the question asked (rather than opining on MISRA) I think the answer there also answers this. – Toby Mar 03 '17 at 12:06
  • @Lundin I do agree though, that this req is unneeded – Toby Mar 03 '17 at 12:08

5 Answers5

43

The second is best because it is so much easier to read, scales well with increased complexity and immediately stops executing the function upon errors. This is the only sensible way to write such functions when you have extensive error handling inside a function, for example if the function is a parser or protocol decoder.

That MISRA-C disallows multiple return statements in a function is a defect of MISRA-C. The intention is supposedly to disallow spaghetti code that returns from all over the place, but dogmatically banning multiple return statements can actually turn code far less readable, as we can see from your example. Imagine if you needed to check 10 different errors. You'd then have 10 compound if statements, which would be an unreadable mess.

I have reported this defect several times to the MISRA committee but they have not listened. Instead, MISRA-C just blindly cites IEC 61508 as source for the rule. Which in turn only lists one questionable source for this rule (IEC 61508:7 C.2.9) and it's some a dinosaur programming book from 1979.

This is not professional nor scientific - both MISRA-C and IEC 61508 (and ISO 26262) should feel ashamed over (directly or indirectly) listing subjective nonsense from 1979 as their only source and rationale.

Simply use the second form and raise a permanent deviation against this defect MISRA rule.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • 2
    I'm agree with you. The second approach is the best when several parameters need to be checked. Specially for code readability. – Federico Mar 03 '17 at 12:39
  • 1
    It's not a *defect*, Lundin :-) ... simple, well structured code is always easier to maintain, than code with gotos, multiple exits etc. – Andrew Dec 03 '17 at 06:52
  • 2
    @Andrew The defect = questionable sources. That being said, it is fairly clear to any C programmer that the 2nd version in the question above is much more readable than the 1st version. Especially as the number of error checks pile up. There's actually 3 alternatives here: 1) nested compound statement madness, 2) return upon error, or 3) "on error goto" à la BASIC. The 1st is unreadable and hard to maintain, the 2nd is very readable and rugged code, the 3rd is acceptable but not as readable as the 2nd. MISRA-C:2004 only allows 1). MISRA-C:2012 only allows 1) and 3). How is it _not_ a defect? – Lundin Dec 04 '17 at 09:27
  • 5
    Rule 15.5 is *Advisory* - if you can justify the decision, disapply it :-) – Andrew Dec 05 '17 at 14:11
  • 1
    @Andrew Oh, it was downgraded? I'm pretty sure it used to be Required in MISRA-C:2004? – Lundin Dec 05 '17 at 14:28
  • 3
    Yes... it was reclassified for :2012 :-) – Andrew Dec 05 '17 at 16:35
  • 3
    I would think a better rule would be to say that a function may have an arbitrary number of early-exit returns that occur before the first side-effect, and may either have multiple returns that follow the last statement that causes a side-effect, or a single return that causes a side-effect itself. I think that should allow most reasonable usage cases while forbidding most unreasonable ones. – supercat May 20 '18 at 01:12
  • 1
    @supercat Whenever you write a function like a protocol decoder or a parser etc, you easily end up with extensive error handling. The clearest way to write such function is always to return upon error. As long as this is not done from inside nested compound statements but from the top level of the function, it is always readable. I'm not sure if it is meaningful to speak of side effects, as the C standard's definition of them, since reading volatile etc is side effects. It is probably more meaningful to speak of modifying parameters & return value. – Lundin May 21 '18 at 07:06
  • 1
    FWIW, I think @supercat provides a useful objective metric. More important, it's neither helpful nor accurate to refer to Ed Yourdon as "some dinosaur". His *Modern Structured Analysis* was part of the last, best serious attempt by CS to rationalize systems analysis and design. I agree with your critique, but I would say we've learned something in the last 40 years, not that Yourdon should be dismissed as irrelevant. – James K. Lowden May 06 '21 at 15:39
  • @JamesK.Lowden: Many "dinosaurs" acquired more wisdom before 1980 than many of today's programmers will acquire in their lifetime. Many technological hurdles to constructing projects that are so complex as to be incomprehensible have gone by the wayside, but the fact that one can construct systems that complex doesn't always mean one *should*. – supercat May 06 '21 at 16:21
  • For the record if someone wants to read: Here is [the IEC 61508-7 standard from 1997](http://www.cechina.cn/eletter/standard/safety/iec61508-7.pdf) and [the referenced book from 1979](http://vtda.org/books/Computing/Programming/StructuredDesign_EdwardYourdonLarryConstantine.pdf) – Miroslaw Opoka Aug 28 '22 at 16:11
  • the second example is undefined if (foo == bar) && (foo1 == bar1) && (foo2 == bar2). Misra rules exist for a reason ... – lumpidu Mar 27 '23 at 09:09
  • @lumpidu But that's only because the OP didn't use the helper variable in the second example (and even the compiler would spot that bug). The code in the question is pseudo code anyway. – Lundin Mar 27 '23 at 09:24
  • No: one needs to take exactly this scenario into account, why multiple return points are often a cause for critical mistakes in C. It makes it too easy to miss not only the default value return value, but also more subtle issues with missing deinititialization, etc. The bigger a function gets (and yes, functions shouldn't be big, but in real-world sceanarios one finds those everywhere in legacy code), the more problematic the second approach is. It's also harder to refactor out code-parts, if there are multiple return statements in a function. But that's another story. – lumpidu Mar 27 '23 at 11:02
4

I agree with the Lundin’s answer but I would like to provide another solution that complies with the single exit rule and still is similarly readable to the second example:

ErrorCode_e myCheckFunction( some params )
{
  ErrorCode_e error = CHECK_FAILED;

  if( foo != bar )
  {
     error = CHECK_FAILED;
  }
  else if( foo_1 != bar_1 )
  {
     error = CHECK_FAILED;
  }
  else if( foo_2 != bar_2 )
  {
     error = CHECK_SUCCESS;
  }
  else
  {
     // else (even empty) is required by MISRA after else-if
  }
  return error;
}

Since there are only two options in the example, we could use just one condition:

ErrorCode_e myCheckFunction( some params )
{
  ErrorCode_e error = CHECK_FAILED;

  if( (foo == bar) && (foo_1 == bar_1) && (foo_2 != bar_2) )
  {
     error = CHECK_SUCCESS;
  }

  return error;
}

This case can be even more simplified, we don’t need any local variables:

ErrorCode_e myCheckFunction( some params )
{
  return ( (foo == bar) && (foo_1 == bar_1) && (foo_2 != bar_2) )
      ? CHECK_SUCCESS : CHECK_FAILED;
}
Melebius
  • 6,183
  • 4
  • 39
  • 52
3

Funny nobody noticed, that the above 2nd example demonstrates, why the MISRA rule exists in the first place: it leaves out a default return value for all cases where the if clauses do not match.

So what happens, if (foo == bar) && (foo1 == bar1) && (foo2 == bar2) ?

Moreover, in the 1st example for me it's easier to grasp, in which special case there is a non-default return value.

lumpidu
  • 719
  • 6
  • 13
  • Yeah and the relevant MISRA:2012 rule to spot that bug is the mandatory 17.4 and not advisory 15.5 which was discussed here. So this little scenario here is not a valid justification for 15.5 since the bug you mention is already covered by 17.4. – Lundin Mar 27 '23 at 09:31
  • One can say 15.5 is the extreme form of 17.4. 15.5 even refers to 17.4. And why should this rule not be justified with the erroneous above example 2 ? If you always follow rule 15.5 those scenarios never happen. But if you just follow 17.4, then you need to trust your IDE, read your compiler logs, use more brain cycles to make sure that you as a reviewer really have seen e.g. all deinit paths. I could just about state as many examples, where code gets more unreadable and bloated with example 2, especially if you need to do cleanups before returning. – lumpidu Mar 27 '23 at 11:52
2

The method I use is goto error_exit.

You have to consider why a function might fail.

Reason 1 is illegal arguments, like passing a negative to a square root. So assert fail, the error is caller's.

Reason 2 is out of memory - that's an inherent problem with functions that scale. You need to shunt the failure up, though normally if a program won't give you a small amount of memory to hold, say, a file path, then it's dead.

Reason 3 is bad grammar. That's a special case of illegal arguments. If the argument is a double for a square root, caller can reasonably be expected to check for negatives. If the argument is a basic program, caller cannot check for correctness except by effectively writing his own parser. So bad grammar needs to be handled as normal flow control.

Reason 4 is malfunctioning hardware. Nothing much you can do except shunt the error up, unless you are familiar with the specific device.

Reason 5 is an internal programming error. By definition there is no correct behaviour because your own code is not correct. But you often need to fudge or throw out degenerate cases in geometry, for example.

The goto error_exit method is the one I favour, however. It keeps the one point of entry . and of exit principle essentially intact, without introducing artificial nesting for memory allocation errors that are less likely to happen than the computer breaking.

Malcolm McLean
  • 6,258
  • 1
  • 17
  • 18
  • In my opinion, a better version than "on error goto" is to let the function be a wrapper around the actual function. Leave all resource allocation/deallocation in the wrapper. It's essentially the same thing as "on error goto" but a bit more object-oriented, separating allocation from the algorithm. And as a bonus, you don't have to suffer the endless "goto considered harmful" debate. – Lundin Mar 03 '17 at 11:44
  • Another answer is to wrap malloc in xmalloc, which is guaranteed not to fail. If it fails it puts up a dialog asking user to free up some memory or terminate. – Malcolm McLean Mar 03 '17 at 11:46
  • While "goto fail" is always an option (although requiring a Deviation for earlier versions of MISRA C, remember that Apple suffered a rather embarrassing SSL bug using that method! – Andrew Dec 03 '17 at 06:49
  • Arguably the bug was caused by not using {} braces on single statement if clauses. However, the bug wouldn't have gone unnoticed or have terrible consequences if a return err; statement had been used instead of goto fail. You could say the bug in question was caused by at least 2 poor practices combined. – EmbeddedSoftwareEngineer Aug 09 '23 at 08:34
2

I tend to use a mix of the two styles, with 2nd style (multiple returns) before, and (perhaps) the first style (local variable to be returned later) afterwards.

The rationale is: "multiple returns" is definitive. It can/should be used when there is something absolutely wrong about the parameters passed, or some other unrecoverable condition.
The "local variable" style, instead, allows to write code that can modify the return value even more than once. It tends to produce code that means "let's start by supposing failure; but if everything is ok, then I will rewrite the result as OK". Or the contrary: "assume OK; if anything goes wrong set the result as failure". And in between of these steps, there still can be other returns!

As last thought... I would say that the right style depends on the situation, never assume one is always right and the other is always wrong.