When writing functions in C, what considerations govern the way in which results are returned from the function? My question arises from reading the following code, which creates a node in a doubly-linked list, and which comes from this book:
typedef struct DLLIST
{
uint16 tag; /* This node's object type tag */
struct DLLIST *next;
struct DLLIST *previous;
void *object;
uint32 size; /* This node's object size */
} DLLIST;
DLLIST *dlCreate(uint16 tag, void *object, uint32 size)
{
DLLIST *newNode;
newNode = malloc(sizeof(*newNode));
if (newNode != NULL)
{
newNode->previous = NULL;
newNode->next = NULL;
newNode->tag = tag;
newNode->size = size;
newNode->object = malloc(size);
if (newNode->object != NULL)
{
memcpy(newNode->object, object, size);
}
else
{
free(newNode);
newNode = NULL;
}
}
return newNode;
}
Indeed, there are many aspects of this function that as a beginner I find perplexing:
- Why is the DLLIST pointer returned this way, rather than being passed as a reference pointer and modified? If you did this, you could free up the return value for use as a status byte that reported function success/failure.
- Why is the second argument not tested to ensure that the pointer is not NULL? Surely passing a NULL pointer to memcpy would be a very bad thing?
Furthermore, is it common practice to force ALL functions within a module to use the same calling convention? For example, should all functions within the doubly-linked list module from which the function above is taken conform to status byte = function(argument list)? In addition, is it common practice to validate ALL function arguments whose value may be incorrect? For example, check all pointer values to ensure they are not null?
Returning the result through a parameter passed-in from the outside is a rather inelegant and cumbersome practice, which should only be used when there’s no other way. It requires the caller to define a recipient object, which is already bad by itself. But on top of that the need for additional declarations precludes the use of the function in pure expressions.
Most of the time, if you can implement your functionality through a pure function, it should be implemented through a pure function. I.e. function parameters are for input, function return value is for output. This is exactly what you see in this case.
Well, no argument, it is always a good idea to perform such tests. However, the exact method of testing can vary, depending on the “level” of the function in the program hierarchy (from “low level” to “high level”). Should it be a debug-only assertion? Should it be a permanent assertion (i.e. a controlled abort, with error log and “call me maybe” message)? Should it be a run-time test that implies some meaningful recovery strategy? There’s no “one true rule” for answering this questions. It is a matter of code design.
Is there a meaningful fail-safe strategy for this function that should be executed in situations when the test fails? If not, then a run-time test in a low level function would make no sense whatsoever. It would only clutter the code with noise and waste performance. If this is indeed a “low level” function, then a run-time test with
ifis not really appropriate here. What is more appropriate is a debug-only assertionassert(object != NULL && size > 0)Note also that the detail that makes the
object != NULLtest appropriate indlCreatespecifically is that theobjectvalue is passed to the library functionmemcpy, which is known to produce undefined behavior in case of null pointer input. If instead ofmemcpythe code used some proprietarymy_copy_datafunction, then the proper place to assert the validity ofobjectwould be the innards ofmy_copy_data. ThedlCreatefunction itself does not really care about the validity ofobjectvalue.