Why return a static pointer instead of an out parameter?
Asked Answered
E

4

18
char* asctime (const struct tm * timeptr);
char* ctime (const time_t * timer);

I found that many functions inside of time.h return pointers to static variables, which could be altered by any subsequent call to those functions. That means I have to copy the data I just got as a result and it's an extra operation I have to execute and that makes those functions thread-unsafe.

Why was it implemented that way? Wouldn't these signatures be better?

void asctime (char * out, const struct tm * timeptr);
void ctime (char * out, const time_t * timer);

We always have to take decisions during development. I'm just asking why they chose to return a static pointer instead of taking an "out variable" as a parameter.

By the way (this is another question), why don't they allocate their result on the heap? Is it to allow the use of anything instead of malloc or just for efficiency?

El answered 9/7, 2019 at 11:22 Comment(5)
Just curious, how do you know that they "...return pointers to static variables"?Felecia
@Felecia From the Linux man page: "The four functions asctime(), ctime(), gmtime() and localtime() return a pointer to static data and hence are not thread-safe."Sw
@Felecia the docs say soBackslide
You can also read the source code of asctime in cplusplus.com/reference/ctime/asctimeHesson
@AgustínNietoGarcía "It is defined with a behavior equivalent to", not "this is the source"Backslide
B
22

The specification of the ctime and asctime functions goes back to C89, and things were done a bit different back in those days, mainly because multi-processor systems weren't very common and thus using a static buffer wouldn't cause a big problem.

Most likely, they didn't return dynamically allocated memory because it took extra time, and in those days CPU cycles were harder to come by.

If you're on a POSIX system like Linux, you have two other functions available which are basically what you described as an alternative:

   char *asctime_r(const struct tm *tm, char *buf);
   char *ctime_r(const time_t *timep, char *buf);

These function take a pointer to a buffer that can receive the output (and they return a pointer to that same buffer). The _r suffix means "reentrant", meaning it can safely be called either in a multithreaded program or more than once without a sequence point in between.

Babineaux answered 9/7, 2019 at 11:30 Comment(2)
The other advantage to returning a pointer to a static buffer is that it's impossible to leak the memory, so you can do things like printf("It is now %s\n", ctime(time(NULL))).Spathose
And the functions know the maximum needed buffer size, so overflowing the buffer isn't possible either.Intrude
G
12

That means I have to copy the data I just got as result

Why do you need to copy it?

Note that even if you copy the data as soon as you get it, you will still be open to races.

Why was it implemented that way?

Because when they were standardized (1989), most software wasn't multi-threaded even if multi-threading existed since the mainframe era. For reference, even POSIX threads were standardized years later (1996) than these functions. It did not help either that computers got quicker every single year and multi-core/SMT processors did not appear until 2001-2006.

If you needed something else, you could always use a system-specific function.

why don't they allocate their result on the heap?

Allocating is very expensive.

Is it to allow the use of anything instead of malloc or just for efficiency?

Not sure what you mean by that. The proper way to do this is to pass a pointer to the destination buffer, so that the user chooses what allocation method to use.

Gradual answered 9/7, 2019 at 11:31 Comment(5)
Multi-core systems appeared in relatively widespread commercial use about 1992. ANSI C was de facto documenting existing codebases which were designed with only single threads of execution in mind. There's also significant pushback against standardizing functions that allocate memory in C itself - note the lack of strdup() in the C standard.Irishirishism
@AndrewHenle Wasn't sun4d multi-CPU rather than multi-core/SMT? I thought Sun didn't get into multi-core until the UltraSPARC IV (2004). Note that multi-CPU computers/mainframes have existed since the 60s/70s; but while you can do multi-threading in multi-CPU systems, it was rare as you say (multi-processing was way more common AFAIK, you didn't need to exploit the benefits of the shared L2/3 caches).Gradual
@Luaan Memory being shared between all the CPUs just makes it SMP, not SMT.Gradual
@Gradual Duh, my bad. I assumed you meant SMP, since SMT shouldn't have any effect on code running on the processor (beyond timing). The main point is that on the hardware level, the sun4d didn't isolate the processors - so you could have software that was fully multi-threaded. I have no idea if the OS/software had any support for multi-threading or if it was exclusively used for multi-processing.Erythrism
@Erythrism There were SMP systems which supported multi-threading many years before the sun4d, like all the mainframes in the 70s with UNIX etc. However, I would imagine there was not much incentive making software multi-threaded. I will edit the answer, nevertheless, to make it more clear.Gradual
B
6

You are (almost) describing the _s variants that were added in C11

errno_t ctime_s(char *buffer, rsize_t bufsz, const time_t *time);
errno_t asctime_s(char *buf, rsize_t bufsz, const struct tm *time_ptr);

These write to the specified location, provided it is big enough, and report the error otherwise.

You don't need to malloc the buffers for these calls, as you know char buf[26]; is exactly what is needed.

Backslide answered 9/7, 2019 at 11:31 Comment(2)
... that were added as an optional feature in C11. In practice, few compilers implement these or the other Annex K functions. Even Microsoft, the source of that whole boondoggle, doesn't implement the *_s functions fully according to the specifications that were accepted into the language standard.Mitochondrion
@JohnBollinger Even Microsoft, the source of that whole boondoggle, doesn't implement the *_s functions fully according to the specifications that were accepted into the language standard. It's worse than that. Microsoft's implementation doesn't even conform with the original TR 24731-1 technical report itself, which I presume originated with significant Microsoft input.Irishirishism
Y
5

C is a product of the early 1970s, and that legacy shows in things like this. strtok also uses a static buffer and is neither thread-safe nor re-entrant.

I've not seen a definitive explanation for why those functions were implemented that way. It may have been to save stack space (128 kB was a lot of very expensive memory at the time), it may have been to avoid runtime checks on the size or validity of the target buffer, etc. C was originally designed for systems programming, so if time calculations were being done a lot, I can see this approach saving a significant number of cycles over the course of the day.

Unfortunately, that's speculation on my part. I agree that passing the target buffer is the better solution, and that should be the path forward.

Yale answered 9/7, 2019 at 14:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.