Unwrapping the Beauty
2024-08-24
Over the years, I've developed a habit of error-checking every single
function call that could go wrong. Yes, this can easily make your code
cluttered, but thankfully, you can create wrappers that perform both the call
and error handling. That is, instead of calling fwrite()
and
checking for error a dozen times, you call myfwrite()
that many
times without any checks because its definition includes error check and
termination. That's okay as long as it's a console program. But what if it's a
GUI application that shouldn't terminate when the document could not be
saved?
Writing a wrapper won't do any good in this situation. If you do, you'll have
to error-check each call to the wrapper as well. So resorting back to
error-checking every fwrite()
call and immediately returning an
error code to the GUI handler is how I'd usually do it. Like this:
on_mni_file_save(): if save_document() != 0: show error save_document(): ... fwrite(), if failed, return -1 ... fwrite(), if failed, return -1 fwrite(), if failed, return -1 ... fwrite(), if failed, return -1 return 0
Recently I started adding XCF support to Vara, and I found myself in a
similar situation. Reducing the number of fwrite()
calls was not an
option. But the logic getting lost in error checks was not something that I
could stand either. Also, naive error-checking had the possibility of missing
checks here or there in future. So I started thinking for a better solution.
I first thought about creating an object that wrapped fp
(the
file pointer) and an error flag, and passing it to an fwrite()
wrapper that would check for as well as record errors. Yes, I would be calling
this wrapper even after some error happens, but it's an overhead that's worth
having. Then I thought, wait, fp
itself has an error flag; why not
check this inside the wrapper? But then I thought, wait again, doesn't
fwrite()
check this flag on its own before proceeding? (glibc's
fwrite()
does this, and others must as well.)
So the solutions in each iteration of thought were like this:
- Create an object that wraps
fp
and an error flag, and then write a wrapper forfwrite()
that would check as well as update this object in every call. - Create an
fwrite()
wrapper that checksferror()
before proceeding. So no custom object. - Just call
fwrite()
. No custom object, nofwrite()
wrapper.
In all these cases, the error check inside save_document()
happens in the very end. Of course, we'll be issuing more fwrite()
calls this way when an error occurs, but it's better than making the code a mess
with all those error checks. Also, if there's an fwrite()
call
inside a loop or something, you can add checks just in those portions to avoid
unnecessary calls.
So this is how the modified code would look like:
on_mni_file_save(): if save_document() != 0: show error save_document(): ... fwrite() ... fwrite() fwrite() ... fwrite() return 0 if fp has no error, -1 otherwise
I'm really glad that I spent a couple more minutes to think instead of jumping on top of the keyboard to write hundreds of lines of abstractions right away.
Note: although we are calling fwrite()
a lot, no unnecessary
switch to the kernel mode is happening since system calls are issued inside
fwrite()
only after checking for errors.
Nandakumar Edamana
Read more from Nandakumar at nandakumar.org/blog/