Date: Tue, 23 Sep 2025 18:49:47 -0700
From: Paul Eggert <[email protected]>
Message-ID: <[email protected]>
| I don't see that as a plausible reading.
There really is no other.
| The text says "one of two static objects",
Yes, the wording is bizarre, attempting to specify the
return data from localtime() and ctime() all in one sentence,
in order to save a paragraph, is idiotic.
| not "a pointer of one of two types".
That distinction is not material, the functions return pointers,
the object the pointer points to is what matters, not the pointer
itself.
| There are two
| static objects, one for localtime/gmtime and the other for
| asctime/ctime, and that's pretty clear from the wording.
If you concentrate only on that first sentence, I can see how you
might come to that conclusion, but the second sentence makes it
absolutely clear that cannot be what is required.
Look at it again:
Execution of any of the functions that return a pointer to one
of these object types may overwrite
First we have "may overwrite" not "shall overwrite" - "may" means the
implementation is permitted to, but not required to.
If there was only permitted to be one struct tm for all of the results,
that "may" would make no sense at all - every call would (necessarily)
overwrite that struct.
But wait, there's more:
| the information in any object of the same type
"In any object of the same type" - how can that make any sense
to say if there's only permitted to be one of them. What other
objects could that "any" be referring to? For that to make any
sense it must be that it is possible that there are more than one.
| And I'm not the only person to read the standards this way.
That I cannot help. Unfortunately, lots of people don't spend the
effort required to really understand what any of these things actually say.
| Although later C standards relax this (and I'll have more to say about
| that in a later email), POSIX.1-2024 still has the C89 requirement.
Yes, while I don't have a C standard that old, the oldest ones I have
have identical wording to POSIX (hardly surprising, POSIX "borrows" the
C standard language as much as it can). If what is there is the same
as in C89, then it has clearly been misinterpreted, all of this time,
as that has both the "may" and "any object of the same type" which make
it clear that there is no such requirement. There never was.
| As you probably know, the standards did not invent this requirement.
No, they didn't, as it doesn't exist.
| In 7th edition Unix,
This predates that, localtime() ctime() (etc) were not new in 7th edition
(though lots of the rest of libc as we know it now was) - what was new
was "struct tm" - in earlier versions that was int tm[9]; (or something like
that) - ie: the return value was a pointer to an array of 9 integers, (to
be strictly correct, it was a pointer to the first integer in that array)
which contained the secs/mins/hours/mday/mon/year/wday/yday/isdst values
(which morphed into struct tm in 7th edition - not actually changing
anything, but giving names to the fields ... if code wanted to treat
it the old way, that still worked as well, type checking back then was
kind of non-existant).
| localtime and gmtime calls always returned the same pointer
Yes, they did, and that's why the standard allows that behaviour.
There is a big difference between allowing it, and requiring it, however.
There's no question that it is permitted to act like that in the
standard (even C23 with its additions), and application code should
not expect anything different.
That does not mean (and never did) that application code is entitled
to assume that that relationship between gmtime() and localtime() existed,
it just has to allow for the possibility that it might.
An implementation if it wanted could (assuming that the _r functions
exist) implement localtime() & gmtime() like
static struct ltm[N];
static int nxtltm = 0;
struct tm *
localtime(time_t *t)
{
if (nxtltm >= N)
nxtltm = 0;
return localtime_r(t, <m[nxtltm++]);
}
static struct gtm[N];
static int nxtgtm = 0;
struct tm *
gmtime(time_t *t)
{
if (nxtgtm >= N)
nxtgtm = 0;
return gmtime_r(t, >m[nxtgtm++]);
}
for whatever non-negative value of N it likes (including 0), and it could
skip gtm & nxtgtm and use ltm and nxtltm in gmtime() as well if it wanted,
and doing that, with N==0, achieves what you believe is required, though
any implementation like this (any value of N) is permitted. This
implementation may overwrite a value in any of the previously returned
structs of the same type. Exactly as the wording says may be done.
The CSRG (BSD 4.4(ish)) man page for this stuff includes (from 1993)
BUGS
Except for difftime() and mktime(), these functions leaves their result
in an internal static object and return a pointer to that object.
Subsequent calls to these function [sic] will modify the same object.
So even way back then (actually, well before then) this behaviour was
recognised as being a bug, and of course, anything which is a bug is
something that's liable to be fixed at any time - no-one should be
relying upon it remaining (but of course, nor should anyone be assuming
that any particular implementation will have fixed it).
The 6th edition man page just says (of the results of localtime() and
gmtime()):
The value is a pointer to an array whose components are ...
While those two functions certainly used the same array for the result,
the manual page doesn't say that (doesn't even really imply it). It
isn't even explicit that "the value" will be the same pointer from two
calls to localtime() (or gmtime()) - though there's no doubt that's how
the implementation worked.
It has simply never been safe for an application to assume that the
result from any of these calls *must* be the same storage space as
any of the others. Similarly it has never been safe to assume that
it won't be - it can happen either way.
kre
ps: not directly relevant, but slightly, as it touches on how sloppy
all of this old wording is, the paragraph in question says:
Execution of any of the functions that return a pointer to
one of these object types may overwrite the information in any
object of the same type pointed to by the value returned from
any previous call to any of them.
That means, for example, that the char [] array that ctime() returns
can be overwritten by another call to ctime(), or a call to asctime()
(as both of those return a char *), but nothing there permits ctime()
to overwrite the information returned by localtime() (or gmtime())
as that is not "information in an object of the same type returned ...".
Yet we all know (I hope) that (until time_t became 64 bits and localtime()
could fail, and the difference that made is immaterial here) that
the implementation of ctime() is (was) just:
char *ctime(time_t *t) { return asctime(localtime(t)); }
and as localtime() clobbers the static struct tm it returns, ctime()
clobbers it as well - even though the standard does not say that is
permitted, that's not an object of the same type as that returned.