In In the middle of qsort, is there a way to stop it? some comments mentioned using setjmp/longjmp
to break out of a call to qsort()
from the comparison function.
The language specification doesn’t seem to say what happens in this case. The only constraint on the comparison function explicitly mentioned is that it must return consistent results.
Does the lack of mention mean it’s implicitly allowed or is it undefined behavior because the result is not explicitly defined?
I think it’s obvious that the state of the array being sorted would be indeterminate. But could it have more wide-ranging impact?
7
I argue that the behavior is undefined under many, if not all, conditions. (To be precise, I don’t think the longjmp
in and of itself necessarily causes UB, but I think it will be difficult or impossible to avoid having the program’s subsequent execution cause UB.)
This is based on 7.1.4p4 (C23 N3220):
The functions in the standard library are not guaranteed to be reentrant and may modify objects
with static or thread storage duration.
Now, the standard does not define “reentrant”. But at the least, this forbids a comparison function from calling qsort
recursively. So it seems to permit an implementation like
void qsort(...) {
_Thread_local bool inside_qsort = false;
if (inside_qsort)
summon_nasal_demons();
inside_qsort = true;
// ... body ...
inside_qsort = false;
}
If longjmp
were called from the comparison function, 7.13.3.1/2-3 (as mentioned in other answers) actually guarantees that inside_qsort
must retain the value true
, such that any future calls to qsort
would summon the demons. So at the very least, I think we can say that if you longjmp
out of qsort
, you cannot safely call qsort
again during the execution of the program.
A trickier question is whether you can safely call any other library function. The common situation where the non-reentrancy of 7.1.4p4 comes up is signal handlers. In that setting, it’s understood that it forbids calling any library function from a signal handler that may have interrupted a library function – not just the same one. For instance, even if you can prove that the only library function your signal handler could have interrupted was fgetc
, that does not mean the handler can safely call fscanf
.
Based on that logic, we might argue that if you longjmp
out of qsort
, it is undefined behavior to call any library function from that point onward, including such functions as exit
and abort
, which are hard to avoid (recall that returning from main
is equivalent to calling exit
, 5.1.2.3.4p1).
On the other hand, that same logic would lead us to the conclusion that it is unsafe to call any library function from within a qsort
comparison function. And it’s obvious that at least some library functions were intended to be guaranteed safe in that context, such for instance strcmp
, although this is nowhere explicitly stated. So perhaps there would be some library functions that you could safely call after longjmp
out of qsort
, but the standard never says what they are.
2
The language specification doesn’t seem to say what happens in this case. The only constraint on the comparison function explicitly mentioned is that it must return consistent results.
qsort()
is distinguished by being one of the very few standard library functions that relies on a user-supplied callback. You are right that its specifications don’t say what happens if a call to the comparison callback does not return, so there is at least room for question. But this seems only slightly distinguished from the case where the same thing happens in a user-defined callback-based sort function. I don’t see a good reason not to expect the behavior to follow longjmp()
‘s specifications:
The
longjmp
function restores the environment saved by the most recent invocation of thesetjmp
macro in the same invocation of the program with the correspondingjmp_buf
argument. […]All accessible objects have values, and all other components of the abstract machine have state,
as of the time thelongjmp
function was called, except that the representation of objects of automatic
storage duration that are local to the function containing the invocation of the corresponding
setjmp
macro that do not havevolatile
-qualified type and have been changed between thesetjmp
invocation andlongjmp
call is indeterminate.
(C23 7.13.3.1/2-3)
I take that as a definition of what happens, so the behavior is not undefined.
As you observe, however, we must allow that qsort()
may have made unspecified changes of substantially any nature to the contents of the array prior to the longjmp
call. It may also be that qsort()
dynamically allocates memory, which would presumably be leaked by the longjmp
. These are the kinds of issues that attend longjmp
calls generally.
4
The only caveat I found has to do with where the array being sorted resides relative to the setjmp
call prior to calling qsort
.
Section 7.13.2.1 regarding the longjmp
function states the following:
All accessible objects have values, and all other components of the
abstract machine have state, as of the time thelongjmp
function
was called, except that the values of objects of automatic storage
duration that are local to the function containing the invocation of
the correspondingsetjmp
macro that do not have volatile-qualified
type and have been changed between thesetjmp
invocation andlongjmp
call are indeterminate.
So if the array to be sorted is defined in the same function that setjmp
is called from, its contents become indeterminate and attempting to access those contents will trigger undefined behavior.
If the array is not defined in the function where setjmp
is called, the array contents seem to be at most unspecified and not indeterminate.
Based on this, it seems the behavior is well-defined provided the above caveat is avoided. However, there is still the possibility of a memory leak depending on the implementation of qsort
.
IMHO, this is not really specific to qsort
. The fact is that any function that is active between the call to setjump
and the call to longjump
will have no possibility to clean any state. State here may refer to allocated memory, or any other thread or static variable. So if one of those functions is neither user provided and re-entrant nor non-user provided and explicitly marked as re-entrant, calling it again may involve undefined behaviour.
Said differently, you can safely use any variable that was defined before the call to the function containing the call to setjump
(*) and any function that was never called or that could return before the call to longjump
.
Here, my understanding is that calling again qsort
would be undefined by the language, but provided the array was defined before the call to the function containing setjump
(*), you can safely save it to a disk file and terminate the program.
(*) Section 7.13.2 §3 regarding the longjmp function states (emphasis mine):
All accessible objects have values, and all other components of the abstract machine have state, as of the time the longjmp function was called, except that the values of objects of automatic storage duration that are local to the function containing the invocation of the corresponding setjmp macro that do not have volatile-qualified type and have been changed between the setjmp invocation and longjmp call are indeterminate.
2
The Standard is somewhat vague with respect to the internals of qsort
, in large measure because any tasks where such details would matter could be accomplished with a user-code function that either behaves just like qsort
in all documented respects, or behaves in some other way which is even better suited to the actual tasks at hand.
The C language and Standard Library evolved in an era before many of today’s commonplace text-processing or other tools existed, and where the fastest way to accomplish many tasks would be to write a quick “throwaway” C program to accomplish precisely what needed to be done. Since many tasks would require sorting, having a qsort() function available was more convenient than having to write a sorting function every time one wanted to write a quick C program that involved sorting.
It seems doubtful that a competently written qsort()
function would need to expressly allocate any outside storage, since an implementation that always sub-sorts whichever partition is found to be smaller before doing the other one will never need to keep track of more than log₂N levels of pending sub-sort operations, and few execution environments would have trouble declaring a couple of automatic-duration arrays of log₂SIZE_MAX
values of type size_t
. Nonetheless, it would be plausible that an implementation might use a variable-length array, and the Standard expressly notes that VLA storage might leak (I know of at least one embedded implementation that uses malloc
/free
for VLA storage).
I would thus recommend that any program that might need to longjmp
out of a quick sort operation include its own version of quick sort rather than using the library version. Indeed, I’d probably recommend that programs implement their own sorting functions in any case. In cases where e.g. one will want to sort a bunch of pointers to some kind of structure, an implementation of qsort
which uses pointer assignments rather than memcpy
will likely be faster than one which is completely agnostic with regard to what is being stored, and one won’t have to guess whether an implementation might use variable length arrays or other external storage.
4