Live data from Hacker News

Passing nothing is surprisingly difficult

davidben.net

21–30 of 80 posts

Re: Passing nothing is surprisingly difficult

#21
post #2

Useful context on the Rust side is this issue [1]. It sounds like some of the author's concerns are addressed already. [1]: https://github.com/rust-lang/unsafe-code-guidelines/issues/4...

This is basically the "define pointer arithmetic for invalid pointers". Which as pointed out in that section, doesn't solve completely the FFI problem.

Re: Passing nothing is surprisingly difficult

#22

There is no problem with memcpy other than that you can't use a null pointer. You can memcpy zero bytes as long as the pointer is valid. This works in a good many circumstances; just not circumstances where the empty array is represented by not having an address at all. For instance, say we write a function that rotates an array: it moves the low M bytes to the top of the array, and shuffles the remaining M - N bytes…

N2464 [0]: there was lots of implementation divergence on what realloc(ptr, 0) did (especially with BSD, which allegedly doesn't free the memory at all?), so they just declared it UB. [0] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf

The BSD people don't understand what little standards they do read. It's unfortunate that we have to spoil the language for their sake.

The requirements in C99 and before are perfectly clear. realloc is described as liberating the old pointer, and then allocates a new one as if by malloc. (Except that it magically has access to both objects so it can transfer the necessary bytes that must be transferred from the old to the new.)

It is perfectly clear what happens when size is zero. No byte can be copied from the old object, if any. The behavior is like free(oldptr) followed by return malloc(newsize).

Your IQ would have to be well below 85 to misunderstand the requirements.

And those requirements are still there; there is still the description of realloc in terms of freeing the old pointer and allocating a new object with malloc.

There was no need to insert a gratuitous removal of definedness for the size zero case, given that malloc handles it.

Applications now have to do this:

  void *sane_realloc(void *ptr, size_t size)
  {
    if (size == 0) {
      // behave literally as required in C99
      free(ptr);
      return malloc(0);
    }

    return realloc(ptr, size);
  }
Supposedly because a few vendors were not able to code this logic in their realloc functions?

Re: Passing nothing is surprisingly difficult

#23

There is no problem with memcpy other than that you can't use a null pointer. You can memcpy zero bytes as long as the pointer is valid. This works in a good many circumstances; just not circumstances where the empty array is represented by not having an address at all. For instance, say we write a function that rotates an array: it moves the low M bytes to the top of the array, and shuffles the remaining M - N bytes…

Is there a rationale for a memory allocator to support zero sized allocations? Is this really just about providing a "technically" valid pointer for the pointer/size pair structure? To me it seems any address is a potentially valid pointer to a zero-sized object. Do allocators really keep track of these null allocations? That would require keeping state for every single address in the worst case... It's very strange.…

> Is there a rationale for a memory allocator to support zero sized allocations?

Mainly, that it has supported that before and programs rely on it.

Programs written to the C99 standard can resize a dynamic vector down to empty with a resize(ptr, 0). The pointer coming from that will be the same as if malloc(0) has been called.

So now, that has been taken away; those programs can now make demons fly out of your nose.

Thank you, ISO C!

> Do allocators really keep track of these null allocations? That would require keeping state for every single address in the worst case...

Implementations of malloc(0) that don't return null are required to return a unique object. To do that, all they have to do is pretend that the size is some nonzero value like 1 byte. (The application must not assume that there is any byte there that can be accessed).

Re: Passing nothing is surprisingly difficult

#24

There is no problem with memcpy other than that you can't use a null pointer. You can memcpy zero bytes as long as the pointer is valid. This works in a good many circumstances; just not circumstances where the empty array is represented by not having an address at all. For instance, say we write a function that rotates an array: it moves the low M bytes to the top of the array, and shuffles the remaining M - N bytes…

Is there a rationale for a memory allocator to support zero sized allocations? Is this really just about providing a "technically" valid pointer for the pointer/size pair structure? To me it seems any address is a potentially valid pointer to a zero-sized object. Do allocators really keep track of these null allocations? That would require keeping state for every single address in the worst case... It's very strange.…

If storing the metadata in the heap, 0 bytes often doesn't even end up a special case. You need to have a case for allocations of some arbitrary number of bytes, and 0 is an arbitrary number of bytes.

Another option is to treat them as being of size 1.

(In theory you could do endless allocations of size 0, and eventually you'd run out of space, even though you've allocated 0 bytes in total. But you end up in exactly that situation, whatever the allocation size, if you don't take bookkeeping overhead into account!)

Re: Passing nothing is surprisingly difficult

#25

Earlier quoted context omitted.

N2464 [0]: there was lots of implementation divergence on what realloc(ptr, 0) did (especially with BSD, which allegedly doesn't free the memory at all?), so they just declared it UB. [0] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf

The BSD people don't understand what little standards they do read. It's unfortunate that we have to spoil the language for their sake. The requirements in C99 and before are perfectly clear. realloc is described as liberating the old pointer, and then allocates a new one as if by malloc. (Except that it magically has access to both objects so it can transfer the necessary bytes that must be transferred from the old…

C99 and C11 have no special treatment for a size of zero. Since "memory for the new object [of size zero] cannot be allocated, the old object is not deallocated and its value is unchanged." (emphasis added). This is exactly what BSD does.

C17 says "If size is zero and memory for the new object is not allocated, it is implementation-defined whether the old object is deallocated" (emphasis added).

What standard, exactly, is BSD violating?

Re: Passing nothing is surprisingly difficult

#26

Earlier quoted context omitted.

Is there a rationale for a memory allocator to support zero sized allocations? Is this really just about providing a "technically" valid pointer for the pointer/size pair structure? To me it seems any address is a potentially valid pointer to a zero-sized object. Do allocators really keep track of these null allocations? That would require keeping state for every single address in the worst case... It's very strange.…

> Is there a rationale for a memory allocator to support zero sized allocations? Mainly, that it has supported that before and programs rely on it. Programs written to the C99 standard can resize a dynamic vector down to empty with a resize(ptr, 0). The pointer coming from that will be the same as if malloc(0) has been called. So now, that has been taken away; those programs can now make demons fly out of your nose.…

> Programs written to the C99 standard can resize a dynamic vector down to empty with a resize(ptr, 0).

C99 has no resize() function. Assuming you mean realloc(), C99 does not guarantee you can use realloc() in this manner.

See also:

https://news.ycombinator.com/item?id=38850575

https://stackoverflow.com/questions/16759849/using-realloc-x...

https://wiki.sei.cmu.edu/confluence/plugins/servlet/mobile?c...

https://developers.redhat.com/articles/2023/07/26/checking-u...

Re: Passing nothing is surprisingly difficult

#27

Earlier quoted context omitted.

The BSD people don't understand what little standards they do read. It's unfortunate that we have to spoil the language for their sake. The requirements in C99 and before are perfectly clear. realloc is described as liberating the old pointer, and then allocates a new one as if by malloc. (Except that it magically has access to both objects so it can transfer the necessary bytes that must be transferred from the old…

C99 and C11 have no special treatment for a size of zero. Since "memory for the new object [of size zero] cannot be allocated, the old object is not deallocated and its value is unchanged. " (emphasis added). This is exactly what BSD does. C17 says "If size is zero and memory for the new object is not allocated, it is implementation-defined whether the old object is deallocated" (emphasis added). What standard, exact…

That all refers to a failing allocation, obviously. If realloc cannot allocate the new object, the old one stays valid.

In the case of size zero, malloc(0) can return null (always). That is ambiguous; it looks like a failure. If realloc literally were to call malloc(0) and then treats the null as a failure, it will return the old object. However, on an implementation where malloc(0) always returns null by design, that would be obviously be poor behavior for its own realloc.

If malloc(0) returns null by design that is not a case where allocating an object failed.

We basically now need this in every program that might resize to zero:

  void *sane_realloc(void *ptr, size_t size)
  {
     if (size == 0) {
       free(ptr);
       return malloc(0);
     }
     return realloc(ptr, size);
  }
The only problem is that if malloc(0) returns null on an implementation where it normally returns an allocated pointer (and thus the call has failed) we don't detect the failure and don't preserve the original object. The application which relies on sane_realloc has to understand that when size is zero, the deallocation always works, whether or not the subsequent malloc does, and so it may get a null pointer on any platform.

Re: Passing nothing is surprisingly difficult

#28

Earlier quoted context omitted.

N2464 [0]: there was lots of implementation divergence on what realloc(ptr, 0) did (especially with BSD, which allegedly doesn't free the memory at all?), so they just declared it UB. [0] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf

The BSD people don't understand what little standards they do read. It's unfortunate that we have to spoil the language for their sake. The requirements in C99 and before are perfectly clear. realloc is described as liberating the old pointer, and then allocates a new one as if by malloc. (Except that it magically has access to both objects so it can transfer the necessary bytes that must be transferred from the old…

I don't think insulting the BSD folks is very nice. They probably had good reasons for their decisions.

Re: Passing nothing is surprisingly difficult

#29

Earlier quoted context omitted.

C99 and C11 have no special treatment for a size of zero. Since "memory for the new object [of size zero] cannot be allocated, the old object is not deallocated and its value is unchanged. " (emphasis added). This is exactly what BSD does. C17 says "If size is zero and memory for the new object is not allocated, it is implementation-defined whether the old object is deallocated" (emphasis added). What standard, exact…

That all refers to a failing allocation, obviously. If realloc cannot allocate the new object, the old one stays valid. In the case of size zero, malloc(0) can return null (always). That is ambiguous; it looks like a failure. If realloc literally were to call malloc(0) and then treats the null as a failure, it will return the old object. However, on an implementation where malloc(0) always returns null by design, tha…

[deleted]
Post reply on HN