12

In my C programs I often need a way to make a string representation of my ADTs. Even if I don't need to print the string to screen in any way, it is neat to have such method for debugging. So this kind of function often comes up.

char * mytype_to_string( const mytype_t *t );

I actually realize that I have (at least) three options here for handling the memory for the string to return.

Alternative 1: Storing the return string in a static char array in the function. I don't need much thinking, except that the string is overwritten at every call. Which may be a problem in some occasions.

Alternative 2: Allocate the string on the heap with malloc inside the function. Really neat since I then won't need to think of the size of a buffer or the overwriting. However, I do have to remember to free() the string when done, and then I also need to assign to a temporary variable such that I can free. and then heap allocation is really much slower than stack allocation, therefore be a bottleneck if this is repeated in a loop.

Alternative 3: Pass in pointer to a buffer, and let the caller allocate that buffer. Like:

char * mytype_to_string( const mytype_t *mt, char *buf, size_t buflen ); 

This brings more effort to the caller. I also notice that this alternative gives me an other option on the order of the arguments. Which argument should I have first and last? (actually six possibilities)

So, which should I prefer? Any why? Is there some kind of unwritten standard among C developers?

1
  • 3
    Just an observational note, most operating systems use option 3 - caller allocates buffer anyhow; tells the buffer pointer and capacity; callee fills buffer and also returns the actual length of string if the buffer is insufficient. Example: sysctlbyname in OS X and iOS – rwong Feb 8 '15 at 0:30
11

The methods I've seen most are 2 and 3.

The user supplied buffer is actually quite simple to use:

char[128] buffer;
mytype_to_string(mt, buffer, 128);

Though most implementations will return the amount of buffer used.

Option 2 will be slower and is dangerous when using dynamically linked libraries where they may use different runtimes (and different heaps). So you can't free what has been malloced in another library. This then requires a free_string(char*) function to deal with it.

4
  • Thanks! I think I like Alternative 3 best as well. However I want to be able to do things like: printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf)); and hence I won't like to return the length used but rather the pointer to the string. The dynamic library comment is really important. – Øystein Schønning-Johansen Feb 8 '15 at 10:34
  • Shouldn't this be sizeof(buffer) - 1 to satisfy the \0 terminator? – Michael-O Nov 9 '16 at 12:27
  • 1
    @Michael-O no the null term is included in the buffer size meaning that the max string that can be put in is 1 less than the passed in size. This is the pattern that the safe string functions in the standard library like snprintf use. – ratchet freak Nov 9 '16 at 12:51
  • @ratchetfreak Thanks for clarification. Would be nice to extend the answer with that wisdom. – Michael-O Nov 9 '16 at 13:34
0

Additional design idea for #3

When possible, also provide the maximum size needed for mytype in the same .h file as mytype_to_string().

#define MYTYPE_TO_STRING_SIZE 256

Now the user can code accordingly.

char buf[MYTYPE_TO_STRING_SIZE];
puts(mytype_to_string(mt, buf, sizeof buf));

Order

The size of arrays, when first, allows for VLA types.

char * mytype_to_string( const mytype_t *mt, size_t bufsize, char *buf[bufsize]); 

Not so important with single dimension, yet useful with 2 or more.

void matrix(size_t row, size_t col, double matrix[row][col]);

I recall reading having the size first is a preferred idiom in the next C. Need to find that reference....

0

As an addition to @ratchetfreak's excellent answer, I would point out that alternative #3 follows a similar paradigm/pattern as standard C library functions.

For example, strncpy.

char * strncpy ( char * destination, const char * source, size_t num );

Following the same paradigm would help to reduce the cognitive load for new developers (or even your future self) when they need to use your function.

The only difference with what you have in your post would be that the destination argument in the C libraries tends to be listed first in the argument list. So:

char * mytype_to_string( char *buf, const mytype_t *mt, size_t buflen ); 
0

I'd echo @ratchet_freak in most cases (maybe with a small tweak for sizeof buffer over 128) but I wanna jump in here with a weirdo response. How about being weird? Why not, besides the problems of getting weird looks from our colleagues and having to be extra persuasive? And I offer this:

// Note allocator parameter.
char* mytype_to_string(allocator* alloc, const mytype_t* t)
{
    char* buf = allocate(alloc, however_much_you_need);
    // fill out buf based on 't' contents
    return buf;
}

And example usage:

void func(my_type a, my_type b)
{
    allocator alloc = allocator_new();
    const char* str1 = mytype_to_string(&alloc, &a);
    if (!str1)
        goto oom;
    const char* str2 = mytype_to_string(&alloc, &b);
    if (!str2)
        goto oom
    // do something with str1 and str2
    goto finish;
oom:
    errno = ENOMEM;
finish:
    // Frees all memory allocated through `alloc`.
    allocator_purge(&alloc);
}

If you did it this way, you can make your allocator very efficient (more efficient than malloc both in terms of allocation/deallocation costs and also improved locality of reference for memory access). It can be an arena allocator that just involves incrementing a pointer in common cases for allocation requests and pool memory in a sequential fashion from large contiguous blocks (with the first block not even requiring a heap allocation -- it can be allocated on the stack). It simplifies error handling. Also, and this might be the most debatable but I think it's practically quite obvious in terms of how it makes it clear to the caller that it's going to allocate memory to the allocator you pass in, requiring explicit freeing (allocator_purge in this case) without having to document such behavior in every single possible function if you use this style consistently. The acceptance of the allocator parameter makes it hopefully quite obvious.

I don't know. I get counter-arguments here like implementing the simplest-possible arena allocator possible (just use maximum alignment for all requests) and dealing with it is too much work. My blunt thought is like, what are we, Python programmers? Might as well use Python if so. Please use Python if these details don't matter. I'm serious. I have had many C programming colleagues who would very likely write not only more correct code but possibly even more efficient with Python since they ignore things like locality of reference while stumbling over bugs they create left and right. I don't see what there is that's so scary about some simple arena allocator here if we are C programmers concerned with things like data locality and optimal instruction selection, and this is arguably much less to think about than at least the types of interfaces that require callers to explicitly free every single individual thing that the interface can return. It offers bulk deallocation over individual deallocation of a sort that's more error-prone. A proper C programmer challenges loopy calls to malloc as I see it, especially when it appears as a hotspot in their profiler. From my standpoint, there has to be more "oomph" to a rationale for still being a C programmer in 2020, and we can't shy away from things like memory allocators anymore.

And this doesn't have the edge cases of allocating a fixed-sized buffer where we allocate, say, 256 bytes and the resulting string is larger. Even if our functions avoid buffer overruns (like with sprintf_s), there's more code to properly recover from such errors that's required which we can omit with the allocator case since it doesn't have those edge cases. We don't have to deal with such cases in the allocator case, unless we truly exhaust the physical addressing space of our hardware (which the above code handles, but it doesn't have to deal with "out of pre-allocated buffer" separately from "out of memory").

-2

Besides the fact that what you’re proposing to do is a bad code smell, alternative 3 sounds best to me. I also think like @gnasher729 that you’re using the wrong language.

2
-3

To be honest, you might want to switch to a different language where returning a string isn’t a complex, work intensive and error prone operation.

You could consider C++ or Objective-C where you could leave 99% of your code unchanged.

Not the answer you're looking for? Browse other questions tagged or ask your own question.