Shoulds vs. Should Nots
I've always used the tried-and-true "Document and Pray" approach when
it comes to conveying the fact that a pointer result from a function
[..] should not be freed by client code.
In my opinion it's a little easier to focus on documenting when users should free a function's output than when they shouldn't. Focusing on "should nots" often opens you up to paranoia contemplating about endless "what if" scenarios as opposed to "shoulds". In C, there's an endless list of things that users shouldn't do with a library, so I think you'll save a lot of time and paranoia with documentation by focusing predominantly on what they should be doing.
That philosophy applies in general to flexible systems. For example, my system is an entity-component system. That has the benefit of so much flexibility but it also opens up doors to nonsensical cases. Like in mine, a scripter can even attach a motion component to a GUI control and have the physics system apply to it. That makes no sense (it would do something physics-related in the scene and wouldn't crash or anything of that sort but would do something really weird to apply physics to the UI), and so instead of trying to handle all those weird scenarios either by forcefully implementing endless system constraints or documenting every single thing that users shouldn't do, I just focus on what they should be doing instead. If they stray off the beaten path and start trying all kinds of things other than what they should do, then they're on their own because it would take a near-infinite amount of time to document all the things that users shouldn't do in a system that offers near-infinite possibilities.
Conventions, Consistency
What I do in my C APIs is always put a *_create
function right next to a *_destroy
function in the header, always favoring that kind of symmetrical design convention when possible. As a result I'm focusing more on the convention making it obvious when clients should destroy things than when they shouldn't. I even have rare cases where the _create
function returns a structure by value instead of a pointer to it where no destruction logic is needed, but I still provide an empty _destroy
function just to preserve the symmetrical design convention. By preserving that symmetry at all times, even when it is not required, the users don't have to worry about which cases involving a create
function require a corresponding destroy
function to be called. There's always a corresponding destroy
function to be called and proper usage dictates that it is called, even when the destroy
function does nothing at all, even when I strongly believe that the destroy
function will never need to do anything at all.
If I broke symmetry just once in the SDK, then it would raise endless questions about which create
functions which a corresponding destroy
to be called -- suddenly the intellectual overhead skyrockets with just one exceptional case. And that leads me to a thought where:
A convention applied with the utmost consistency can more effectively
communicate usage patterns than any amount of documentation.
However, it requires utmost consistency, since as with the above scenario, a single exception to the rule suddenly destroys all the benefits. 99% consistency is arguably even worse than 0% consistency, since the 99% scenario might have users stop asking questions except when things go wrong, at which point they might be asking endless paranoid questions. The 0% consistency case at least has them asking questions in every single case. 100% consistency is the ideal, and 99% consistency is arguably the worst thing possible.
As a blatant example of why 99% consistency is so bad, we're all used to a floppy disk icon being used to save files. Well, imagine if there was just one rare case where someone used it to format your hard drive. 99% consistency is a horrible thing. I've often been the annoying nit in team environments when it comes to demanding consistency and documented conventions just in the SDK (the public APIs used far beyond our team), but it's because I see 99% consistency as worse than 0%.
It's a fairly rare case in my system for a function that doesn't fit the above convention to output anything with the client's responsibility to call another function to free the memory (actually maybe non-existent, I'd have to double-check), and in no cases in my API should the client ever use free
on anything related to the SDK. That could cause problems across module boundaries. So even for functions that output variable-sized strings, I provide a corresponding symmetrical function to destroy the string. It is never up to the client to free
directly.
However, I try to minimize the number of cases that leave the client to destroy anything manually that don't fit the symmetrical *_create
and *_destroy
convention. Whenever possible, I prefer to receive a pointer to write to with memory already allocated by the client (on stack or heap). If the output is variable-sized, then I have a separate function typically that communicates the size of the data the client needs to allocate and free themselves prior to calling the function which fills out that array.
Self-Documenting Code
That's just what has worked best for me personally, though on to your question:
My question is whether or not it could be considered common practice
(I've never seen it before) to use this sort of jiggery-pokery to try
and get superfluous or even questionable language constructs to
"self-document" an API?
I think short of very symmetrical conventions applied consistently everywhere, most C code can't practically hope to be self-documenting about issues like these (which relate to client responsibilities). const
can be very useful to convey read-only intentions, but I don't find it very clear or useful to convey resource management responsibilities to the client. After all, even a pointer to a const struct Foo*
could still have a field that needs to be freed, and such a field would then only be read-only with respect to the pointer, not the pointee. The pointer field would be free to be copied and the pointee would be free to be modified at any given point.
Just in general a pointer can represent so many different things. Is struct Foo*
a pointer to a contiguous array of Foos
or a pointer to a single Foo
? The type never communicates the difference, and so I prefer to use struct Foo array[]
or something to this sort in function parameters as opposed to struct Foo* array
. It makes no difference at compile-time, but it does help a little bit in communicating the idea that the function expects an array.
C++ developers might have an easier time with self-documenting code in this respect since they have their classes with destructors and copy constructors and all that stuff, but I wouldn't want any of it in C as the appeal of C to me is being able to work with the type system in a way that can assume that none of this stuff exists, like being able to memcpy
data without having to know anything about it. That inevitably comes with the trade-off that we have to establish clear conventions and documentation about client responsibilities. It's something to accept as I see it and come up with clear documentation standards rather than trying too hard to come up with self-documenting code, at least in this one area.