Why can't you just check if errno is equal to ERANGE? [duplicate]
Asked Answered
T

2

9

I've been trying to properly convert a char array to a long with strtol, check if there was an overflow or underflow and then do an int cast on the long. Along the way, I've noticed a lot of code that looks like this

if ((result == LONG_MAX || result == LONG_MIN) && errno == ERANGE)
{
   // Handle the error
}

Why can you not just say

if(errno == ERANGE)
{
    // Handle the error
}

From my understanding, if an underflow or overflow occur, errno is set to ERANGE in both cases. So is the former really necessary? Could checking ERANGE alone be problematic?

This how my code looks as of now

 char *endPtr;
 errno = 0;
 long result = strtol(str, &endPtr, 10);

 if(errno == ERANGE)
 {
     // Handle Error
 }
 else if(result > INT_MAX || result < INT_MIN)
 {
    // Handle Error
 }
 else if(endPtr == str || *endPtr != '\0')
 {
     // Handle Error
 }

 num = (int)result;
 return num;

If there is a reason for the former please let me know.

Travis answered 18/3, 2016 at 1:7 Comment(6)
I've never seen a good explanation for why it is necessary to check for both LONG_MAX/LONG_MIN and ERANGE. Apart from the fact that the man page shows that as an example. The only sensible use case I can think of is to differentiate between overflow and underflow. I too would be interested to hear whether there are other reasons.Grammar
@Grammar I don't know I feel that my example is right mostly because I don't intend to differentiate whether an overflow or underflow occurred and that errno is set to ERANGE in both cases. If either one of them occurred, than the result is invalid.Travis
@LuisAverhoff Maybe I wasn't clear. But I was agreeing with you that your version of error checking looks fine to me.Grammar
@Grammar Opps, sorry about that man.Travis
@Grammar There is one valid check that I forgot to do and I think man page even talks about and that is if errno != 0 && result == 0. Though to be honest, I'm not sure what could cause that because if strtol returns a 0, then I'm positive strtol does not set errno to anything assuming it was initially set to zero.Travis
Your 2nd code snippet is more correct than the 2 answers. IMO it is correct and optimal other than I would just do if(errno)Monogamist
T
8

The first code snippet is just plain wrong, and I'll explain why later, but first we need some background.

errno is a thread-local variable. It is set to a non-zero value when a system call or certain library functions fail. It remains unchanged when a system call succeeds. So it always contains the error number from last call that failed.

This means that you have two choices. Either set errno to 0 before each call, or use the standard idiom for errno. Here's the pseudo-code for the standard idiom

if ( foo() == some_value_that_indicates_that_an_error_occurred )
    then the value in errno applies to foo
else
    foo succeeded and the errno must be ignored because it could be anything

Most programmers will use the standard idiom, because setting errno to 0 before every system call is annoying and repetitive. Not to mention the fact that you might forget to set errno to 0 in the one place it actually matters.


Back to the first code snippet. It is wrong because there is no return value from strtol that unambiguously indicates that strtol failed. If strtol returns LONG_MAX, it could be that an error occurred, or the string actually contained the number LONG_MAX. There's no way to know whether the strtol call succeeded or failed. Which means that the standard idiom (which is what the first code snippet is trying to implement) cannot be used with strtol.

To use strtol correctly, you need to set errno to 0 before the call, like this

errno = 0;
result = strtol( buffer, &endptr, 10 );
if ( errno == ERANGE )
{
    // handle the error
    // ERANGE is the only error mentioned in the C specification
}
else if ( endptr == buffer )
{
    // handle the error
    // the conversion failed, i.e. the input string was empty,
    // or only contained whitespace, or the first non-whitespace 
    // character was not valid
}

Note that some implementations define other non-zero values for errno. See the applicable man page for details.

Theoretics answered 18/3, 2016 at 2:33 Comment(9)
Good answer, but one point: strictly speaking, strtol is a library function, not a system call. And it's unusual in that it does manipulate errno. Most (all?) system calls set errno when they fail, but few library functions do.Profant
Thanks for the great response. If you don't might me asking but is it possible for errno != 0 && result == 0? I have seen code where they incorporate that and if((result == LONG_MAX || result == LONG_MIN) && errno == ERANGE) Like if you set errno to zero right before you call strol to convert A123. You expect result to be 0 and errno to stay at zero. What could cause errno to not be zero in that situation?Travis
@SteveSummit That's true, but I'm not sure how to incorporate that into the answer without introducing needless complexity. I hope it's ok to leave it as a comment.Theoretics
@LuisAverhoff strtol will return 0 if no conversion could be performed. In that case the value in errno is implementation defined. The C spec doesn't specify what value should be stored in errno in that case. So yes, you could have a return value of 0, and a non-zero errno.Theoretics
Sorry to sound like a nitpicker, but I was surprised to read "It is set to a non-zero value when a system call fails", as if strtol was a system call. You could change that to "It is set to a non-zero value when a system call or certain library functions fail", and also change "set errno to 0 before each system call" to "set errno to 0 before each call".Profant
@Theoretics So if it is implementation defined than there is no point in checking for that or if errno = ERANGE if you are checking if there was an overflow or underflow.Travis
@LuisAverhoff Yup, I had to rethink that part. I've updated the answer.Theoretics
Disagree with "To use strtol correctly" as it does not check the case of "no conversion" Need to add if (buffer == endptr) Handle_NoConverisonError();Monogamist
@SteveSummit: Most library functions are not specified by the C standard to set errno, but many of them do so anyway (or are required by POSIX to do so). fopen is an example.Acrocarpous
P
3

If you call

result = strtol("-2147483648", NULL, 0);

or

result = strtol("2147483647", NULL, 0);

on a 32-bit machine, you're going to get LONG_MIN or LONG_MAX in result, even though there hasn't been an error.

As user3386109 explained, one way to detect errors from strtol is to set errno to 0 first. The other way is to let it give you an end pointer and look at that. There are three or four cases:

char *endptr;
long int result = strtol(str, &endptr, 10);
if(*str == '\0') {
    /* str was empty */
} else if(endptr == str) {
    /* str was completely invalid */
} else if(*endptr != '\0') {
    /* numeric result followed by trailing nonnumeric character(s) */
} else {
    /* str was a completely valid number (perhaps with leading whitespace) */
}

Depending on your needs, the first two or three cases may be collapsed together. You may then need to worry (a) whether the "completely valid number" was representable (which you can test using errno), and (b) whether any "trailing nonnumeric character(s)" were innocuous whitespace (which, alas, strtol doesn't check for you, so if you care you'll have to check yourself).

Profant answered 18/3, 2016 at 2:46 Comment(6)
so there is no point in saying `if(result > INT_MAX || result < INT_MIN) because result is always going to equal to either INT_MAX or INT_MIN if there is an overflow or underflow in regards to an int correct? I should just check if result is equal to either one of them.Travis
@LuisAverhoff Sorry, I said INT_MAX when I meant LONG_MAX. (Fixed now.) But in general I'd say there's never any reason to explicitly test the return value of strtol against any min or max value. (The only reason might be to compare against INT_MIN and INT_MAX if you were about to store the value into a plain int.)Profant
Yes I was about to store the value into a plain int.Travis
@SteveSummit My turn to nitpick ;) Trailing whitespace won't be consumed, so *endptr could point to a space character or newline. Specifically, the word "trailing" in the last comment isn't true.Theoretics
@Theoretics Thanks. I coulda sworn I read about a version that stripped trailing whitespace, and it's been in my head that way ever since, but you're right, the documentation doesn't mention it. Fixed.Profant
if(*str == '\0') as a special case is not truly needed as an empty string will get caught in if(endptr == str)Monogamist

© 2022 - 2024 — McMap. All rights reserved.