First, let’s explain what (((size_t*)ptr)[-1])
does, assuming that it is valid:
(size_t*)ptr
converts ptr
to the type “pointer to size_t
”.
((size_t *)ptr)[-1]
is, by definition1, equivalent to *((size_t *) ptr - 1)
.2 That is, it subtracts 1 from (size_t *) ptr
and “deferences” the resulting pointer.
- Pointer arithmetic is defined in terms of array elements and treats a single object as an array of one element.2 If
(size_t *) ptr
is pointing “just beyond” a size_t
object, then *((size_t *) ptr - 1)
points to the size_t
object.
- Thus,
(((size_t*)ptr)[-1])
is the size_t
object that is just before ptr
.
Now, let’s discuss whether this expression is valid. ptr
is obtained by this code:
void * my_malloc(size_t s)
{
size_t * ret = malloc(sizeof(size_t) + s);
*ret = s;
return &ret[1];
}
If malloc
succeeds, it allocates space for any object of the requested size.4 So we can certainly store a size_t
there5, except that this code ought to check the return value to guard against allocation failure. Furthermore, we may return &ret[1]
:
&ret[1]
is equivalent to &*(ret + 1)
, which is equivalent to ret + 1
. This points one beyond the size_t
we have stored at ret
, which is valid pointer arithmetic.
- The pointer is converted to the function return type,
void *
, which is valid.5
The code shown in the question does only two things with the value returned from my_malloc
: retrieve the stored size with ((size_t*)ptr)[-1]
and free the space using (size_t*)ptr - 1
. These are both valid since the pointer conversion is appropriate and they are operating within the limits of pointer arithmetic.
However, there is a question about what further uses the returned value can be put to. As others have noted, while the pointer returned from malloc
is suitably aligned for any object, the addition of a size_t
produces a pointer that is suitably aligned only for an object whose alignment requirement is not stricter than size_t
. For example, in many C implementations, this would mean the pointer could not be used for a double
, which often requires eight-byte alignment while size_t
is merely four bytes.
So we immediately see that my_malloc
is not a full replacement for malloc
. Nonetheless, perhaps it could be used only for objects with satisfactory alignment requirements. Let’s consider that.
I think many C implementations would have no trouble with this, but, technically, there is a problem here: malloc
is specified to return space for one object of the requested size. That object can be an array, so the space can be used for multiple objects of the same type. However, it is not specified that the space can be used for multiple objects of different types. So, if some object other than a size_t
is stored in the space returned by my_malloc
, I do not see that the C standard defines the behavior. As I noted, this is a pedantic distinction; I do not expect a C implementation to have a problem with this, although increasingly aggressive optimizations have surprised me over the years.
One way to store multiple different objects in the space returned by malloc
is to use a structure. Then we could put an int
or a float
or char *
in the space after the size_t
. However, we cannot do so by pointer arithmetic—using pointer arithmetic to navigate the members of a structure is not fully defined. Addressing structure members is properly done by name, not pointer manipulations. So returning &ret[1]
from my_malloc
is not a valid way (defined by the C standard) to provide a pointer to space that may be used for any object (even if the alignment requirement is satisfied).
Other Notes
This code improperly uses %u
to format a value of type size_t
:
printf("%u\n", allocated_size(array));
The specific integer type of size_t
is implementation-defined and might not be unsigned
. The resulting behavior may not be defined by the C standard. The proper format specifier is %zu
.
Footnotes
1 C 2018 6.5.2.1 2.
2 More precisely, it is *((((size_t *) ptr)) + (-1))
, but these are equivalent.
3 C 2018 6.5.6 8 and 9.
4 C 2018 7.22.3.4.
5 A very pedantic reader of C 2018 7.22.3.4 could object that size_t
is not an object of the requested size but is an object of smaller size. I do not believe that is the intended meaning.
6 C 2018 6.3.2.3 1.