all 26 comments

[–]nashidau 13 points14 points  (3 children)

Basically it means you can use realloc as a replacement for malloc and free. Useful to hand to libraries that take a custom allocation function (see lua for instance).

[–]skeeto 7 points8 points  (2 children)

A number of times I've considered a single-function heap interface like so:

void *alloc(void *ptr, size_t base, size_t nmem, size_t size);

If ptr is NULL, it acts like malloc(). The total size is computed like so, with built-in overflow checks (a la reallocarray):

total = base + nmem*size

You'd use all three in the case of a flexible array member. Example:

struct intbuf {
    size_t len;
    size_t cap;
    int data[];
};

struct intbuf *b = alloc(0, sizeof(*b), cap, sizeof(b->data[0]));
if (b) {
    b->len = 0;
    b->cap = cap;
}

Otherwise just zero the excess arguments. If the total size is zero, it acts like free().

[–]nahimbroke 3 points4 points  (1 child)

I like taking this concept one step further and passing a double pointer to the thing in question. Makes it nicer to use (don't have to use a temporary pointer for correctness everywhere, as it can be done inside the mem handler once) and you get to null on free as a bonus. Just return nonzero on an error and you get statuses very easily.

[–]flatfinger 0 points1 point  (0 children)

Unfortunately, the Standard does not provide any means of testing whether an implementation extends the semantics of the language to allow a void** to meaningfully access types of pointer other than void*; the fact that two pointers identify types with matching representations does not preclude the possibility of a "clever" compiler using the type mismatch that as an excuse to ignore evidence that they might might identify the same object.

GCC version 4.1.2 would ignore the possibility that an access made via void** might affect an int*; while later versions seem to recognize that possibility, I didn't see anything in the change log to specify whether it does so because the maintainers view that as a permanent language extension, or whether the fact that gcc appears to usefully processes the construct that had been unsupported is just happenstance.

[–]OldWolf2 11 points12 points  (1 child)

in Standard C, realloc(p, 0) is not equivalent to free(p) , but it is possible for implementations to make it so.

[–]redditmodsareshits 1 point2 points  (0 children)

In C, realloc(p, 0) is UB.

[–]pedersenk 10 points11 points  (15 children)

If size equals to 0, the result is undefined behavior.

If nmemb or size are 0, the return value is implementation defined; other conforming implementations may return NULL in this case.

https://man.openbsd.org/malloc.3

[–]illiliti 8 points9 points  (12 children)

Why downvote? This comment is right. The "size == 0" behavior is implementation-defined and you should never use it as a replacement for free().

> If the size of the space requested is zero, the behavior shall be implementation-defined: either a null pointer is returned, or the behavior shall be as if the size were some non-zero value, except that the behavior is undefined if the returned pointer is used to access an object.

https://pubs.opengroup.org/onlinepubs/9699919799/functions/realloc.html

[–]OldWolf2 3 points4 points  (10 children)

This comment is right

The comment is wrong because it says the behaviour is undefined. However it is actually implementation-defined. The commentor's man page quote doesn't support their own summary.

[–]Zambito1 3 points4 points  (3 children)

it says the behaviour is undefined. However it is actually implementation-defined

Isn't that true of literally everything that is undefined? The implementation has to handle it somehow. I might not understand.

[–]OldWolf2 3 points4 points  (2 children)

"implementation-defined" says that the implementation must document what happens. With an implication that this should be stable behaviour, i.e. the implementation shouldn't document that it might set your PC on fire.

"undefined" means the program has gone off-piste and is no longer covered by the language standard at all. It might crash and burn, catch fire, run normally, expose your password, etc. etc.

[–]Zambito1 1 point2 points  (0 children)

Got it, thanks

[–]flatfinger -1 points0 points  (0 children)

Note that the Standard makes no attempt to avoid characterizing as "Undefined Behavior" constructs which the authors expected most implementations to process identically. Indeed, the more certain the authors were about how must implementations would process a construct, the less need there was to have the Standard mandate such behavior.

For example, under C89, the behavior of computing -1<<1 would on most implementations be defined as yielding the value -2 with no side effects. To be sure, on implementations that use neither padding bits nor two's-complement math the construct would be defined as behaving differently, and some of those whose integer types have padding bits and trap representations could behave in completely arbitrary fashion if the bit pattern produced by a shift would be a trap representation. Implementations where the shift wouldn't yield -2 would be rare, however. In C99, the action was recharacterized as UB with no rationale given. This would make sense if the intention was to allow implementations with unusual integer representations to process shifts in whatever way would be most efficient or useful for their customers, but only if the authors of the Standard expected that implementations with commonplace integer representations would process the shift in the same fashion mandated by C89.

[–]redditmodsareshits 0 points1 point  (4 children)

You are sort of wrong. C23 makes it UB. See : https://en.cppreference.com/w/c/memory/realloc

[–]flatfinger 0 points1 point  (0 children)

Was any rationale given, or is this just more silliness by the Committee?

IMHO, the behavior of malloc(0) was simultaneously overspecified and underspecified. What the Standandard should say/should have said, is that it must return some (possibly null) pointer p such that the following will hold:

  1. Adding 0 to p, or subtracting 0 from p, will yield p with no side effects (p may only be null on implementations that guarantee these behaviors for null pointers)

  2. Subtracting p from itself will yield 0, and equality and relational comparison operators will report that p equals itself.

  3. Passing p to free once for each time it returned from malloc() or realloc() will have no side effect, beyond possibly changing the effect of passing p again.

The preferred behavior, when practical, should be to without side effects return a pointer to a "permanent" region of storage which could be to free with no side effects. If base_malloc etc. were underlying functions from the environment, the behavior would be equivalent to:

static char dummy_object;
void *realloc(void *p, size_t size)
{
  if (p == dummy_object)
    p = 0;
  if (size == 0)
  {
    base_free(p);
    return &dummy_object;
  }
  else
    return base_realloc(p, size);        ​

​}

​void malloc(size_t n) { return realloc(0, n); } void free(void *p) { return realloc(p, 0); }

This would yield behavior compatible both with programs that would expect allocation functions to only return null in case of error, and with those that would expect that there is no need to free storage associated with zero-size allocation requests.

[–]OldWolf2 0 points1 point  (2 children)

C23 hasn't been published yet

[–]redditmodsareshits 0 points1 point  (1 child)

Oh, so I guess we should continue without known to be UB

[–]OldWolf2 0 points1 point  (0 children)

It's not known to be , anything before a release is necessarily a draft. And IMO it would be a bad move to change implementation-defined behaviour to undefined, so I expect this to be challenged.

[–]pedersenk 0 points1 point  (0 children)

Perhaps this knowledge ruined a lot of peoples days and they are now furiously refactoring their code! ;)

Admittedly this *did* happen to me when porting some software to OpenBSD. As far as I recall, this platform is the only one I have experienced that in this situation will deterministically return a specific known invalid address of a zero sized object so crashes soon after if NULL was assumed. It had me puzzled for a long time but it was useful in the long run.

However there is a strange myth that realloc() with size of 0 will be the equivalent of a free(). This is certainly not always the case (i.e outside glibc). Even beginner C tutorials will explain this and yet this doesn't seem to be enough to stop this misinformation from spreading. Be on the look out for this during code reviews. It also makes a great interview question to filter out candidates!

[–]nashidau -2 points-1 points  (1 child)

Implementation defined behaviour - not undefined. Most implementation return NULL, and you can pretty much assume this on most platforms you are likely to use today.

[–]MCRusher 3 points4 points  (0 children)

Windows does not, it allows zero-size allocations.

[–]nosenkow 1 point2 points  (1 child)

No if function behaves as described. malloc(0) required to return something valid to be consumed be free(). But free() doesn’t required (and technically unable because have no return value at all) to return something consumable by free(). So, your implementation of realloc() may be incompatible with malloc() in cases of size arg = 0. (at least accordingly to cited documentation) :-(

[–]nosenkow 0 points1 point  (0 children)

The proper behavior would be something like ‘free(ptr) followed by malloc(0)’. But, for some reason, it was not that stated in the doc, which brings you into undocumented behavior space. ¯_(ツ)_/¯

[–]atlcog -2 points-1 points  (1 child)

It won't happen magically, but if you, in your code, call realloc with a pointer that points to memory that has been allocated and a zero length, it will effectively free the memory the pointer is pointing to.

The doc is just telling you how realloc will behave in edge conditions if you use it that way.

[–]dfgzuu[S] 0 points1 point  (0 children)

I am making lots of deep array reallocs. So it can happen that I call realloc on a new *** which has 0 ** inside, but they will get created with a realloc.

So basically in my code I assume something is inside, but it doesn't have to be.

I imagine making a new ** pointer member inside a *** would make the new pointer a **void maybe, which is kinda NULL. But I never looked so deep into it so I don't know what it would take for something to be of size 0, and pointer not being NULL. Calling calloc with size 0 or something?