Search code examples
cfgets

fgets implementation from the std library of K&R 2nd edition


I'm aware that code snippets from textbooks are just for demonstration purposes and shouldn't be held to production standards but K&R 2nd edition on page 164-165 says:

fgets and fputs, here they are, copied from the standard library on our system:

char *fgets(char *s, int n, FILE *iop)
{
    register int c;
    register char *cs;
    
    cs = s;
    while (--n > 0 && (c = getc(iop)) != EOF)
        if ((*cs++ = c) == '\n')
            break;
    *cs = '\0';
    return (c == EOF && cs == s) ? NULL : s;
}

Why is the return statement not return (ferror(iop) || (c == EOF && cs == s)) ? NULL : s; since:

  1. ANSI C89 standard says:

If a read error occurs during the operation, the array contents are indeterminate and a null pointer is returned.

  1. Even the standard library illustrated in Appendix B of the book says so. From Page 247:

fgets returns s, or NULL if end of file or error occurs.

  1. K&R uses ferror(iop) in fputs implementation given just below this fgets implementation on the same page.

With the above implementation, fgets will return s even if there is a read error after reading some characters. Maybe this is an oversight or am I missing something?


Solution

  • You are correct that the behavior of the posted implementation of the function fgets does not comply with the C89 standard. For the same reasons, it also does not comply with the modern C11/C18 standard.

    The posted implementation of fgets handles end-of-file correctly, by only returning NULL if not a single character has been read. However, it does not handle a stream error correctly. According to your quote of the C89 standard (which is identical to the C11/C18 standard in this respect), the function should always return NULL if an error occurred, regardless of the number of characters read. Therefore, the posted implementation of fgets is wrong to handle an end-of-file in exactly the same way as a stream error.

    It is worth noting that the second edition of K&R is from 1988, which is from before the ANSI C89 standard was published. Therefore, the exact wording of the standard may not have been finalized yet, when the second edition of the book was written.

    The posted implementation of fgets also does not comply with the quoted specification of Appendix B. Assuming that the function fgets is supposed to behave as specified in Appendix B, then the posted implementation of fgets handles errors correctly, but it does not handle end-of-file correctly. According to the quote from Appendix B, the function should always return NULL when an end-of-file occurs (even if characters have been successfully read, which is not meaningful).

    It is also worth noting that using the statement

    return (ferror(iop) || (c == EOF && cs == s)) ? NULL : s;
    

    as suggested in the question will not make the implementation of the function fgets fully comply with the C89/C11/C18 standards. When a stream error occurs "during the operation", the function is supposed to return NULL. However, when ferror returns nonzero, it may be impossible to tell whether the error occurred "during the operation", i.e. whether the stream's error indicator was already set before the function fgets was called. It is possible that the stream's error indicator was already set due to an error that occurred before fgets was called, but that all subsequent stream operations succeeded or failed due to end-of-file (i.e. not due to stream error). The function fgets is also not allowed to simply call clearerr at the start of the function in order to distinguish these cases, because it would then have to restore the state of the stream's error indicator before returning. Setting the stream's error indicator is not possible in the C standard library; it would require an implementation-specific function. Looking at the return value of getc will not always be able to resolve this ambiguity, because a return value of EOF can mean both end-of-file or error.