Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alignment of array with 0 elements

Tags:

C++ allows dynamic allocation of zero-sized arrays:

int* p = new int[0]; delete[] p; 

I can't do much with such a pointer (as the array has no elements), but the new expression is required to give me back a valid (!= nullptr) pointer which I then have to delete[] again as if it was an actual array.

Are there any requirements regarding the alignment of the memory returned by such a new expression? Consider:

struct alignas(8) Foo {     int x; };  Foo* p = new Foo[0]; delete[] p; 

Is p guaranteed to point to an 8-aligned address? Furthermore, if I write a custom allocator, am I required to return pointers to aligned addresses in such a case?

like image 699
ComicSansMS Avatar asked Dec 16 '17 08:12

ComicSansMS


People also ask

Can size of array be 0?

Although the size of a zero-length array is zero, an array member of this kind may increase the size of the enclosing type as a result of tail padding. The offset of a zero-length array member from the beginning of the enclosing structure is the same as the offset of an array with one or more elements of the same type.

What is array alignment?

The alignment attribute specifies the alignment of variables or structure fields, not single array elements. See Specifying Attributes of Variables and Common Variable Attributes for details. If you always want to align two integers together, you can define a structure.


1 Answers

basic.stc.dynamic.allocation/2 of N3337 (basically C++11):

The allocation function attempts to allocate the requested amount of storage. If it is successful, it shall return the address of the start of a block of storage whose length in bytes shall be at least as large as the requested size. There are no constraints on the contents of the allocated storage on return from the allocation function. The order, contiguity, and initial value of storage allocated by successive calls to an allocation function are unspecified. The pointer returned shall be suitably aligned so that it can be converted to a pointer of any complete object type with a fundamental alignment requirement (3.11) and then used to access the object or array in the storage allocated (until the storage is explicitly deallocated by a call to a corresponding deallocation function). Even if the size of the space requested is zero, the request can fail. If the request succeeds, the value returned shall be a non-null pointer value (4.10) p0 different from any previously returned value p1, unless that value p1 was subsequently passed to an operator delete. The effect of dereferencing a pointer returned as a request for zero size is undefined.

Fundamental alignment (basic.align/2):

A fundamental alignment is represented by an alignment less than or equal to the greatest alignment supported by the implementation in all contexts, which is equal to alignof(std::max_align_t)

Extended alignment (basic.align/3):

An extended alignment is represented by an alignment greater than alignof(std::max_align_t).

It is implementation-defined whether any extended alignments are supported and the contexts in which they are supported

So, the returned pointer by operator new must have fundamental alignment. Even if zero size specified. And it is implementation defined, whether 8 is fundamental or extended alignment. If it is fundamental, then Foo is OK. If it is extended, then it is implementation defined that Foo is supported with operator new.

Note, that for C++17, the situation is improved:


basic.stc.dynamic.allocation/2 of C++17:

The allocation function attempts to allocate the requested amount of storage. If it is successful, it shall return the address of the start of a block of storage whose length in bytes shall be at least as large as the requested size. There are no constraints on the contents of the allocated storage on return from the allocation function. The order, contiguity, and initial value of storage allocated by successive calls to an allocation function are unspecified. The pointer returned shall be suitably aligned so that it can be converted to a pointer to any suitable complete object type ([new.delete.single]) and then used to access the object or array in the storage allocated (until the storage is explicitly deallocated by a call to a corresponding deallocation function). Even if the size of the space requested is zero, the request can fail. If the request succeeds, the value returned shall be a non-null pointer value ([conv.ptr]) p0 different from any previously returned value p1, unless that value p1 was subsequently passed to an operator delete. Furthermore, for the library allocation functions in [new.delete.single] and [new.delete.array], p0 shall represent the address of a block of storage disjoint from the storage for any other object accessible to the caller. The effect of indirecting through a pointer returned as a request for zero size is undefined.

I've put emphasis on the relevant part. That sentence means that the returned pointer of void *operator new(...) should have suitable alignment. It doesn't mention zero size as a special case (but, of course, it is UB to dereference the returned pointer).

So the answer is the usual, there is no special handling of zero:

  1. void *operator new(std::size_t) must return an aligned pointer of alignof(std​::​max_­align_­t)
  2. void *operator new(std::size_t, std::align_val_t align) must return an aligned pointer of align)

Note that it is implementation defined, which version will be called for Foo. It depends on whether 8 is equal or less than alignof(std​::​max_­align_­t). If it is less, then the 1st version is called (because it doesn't have extended alignment). Otherwise the 2nd is called.


UPDATE: As Massimiliano Janes comments, these paragraphs apply to the result of operator new, not to the result of new expression. An implementation could add an arbitrary offset to the result of operator new[]. And the standard is silent about the value of this x offset:

new T[5] results in one of the following calls:

operator new[](sizeof(T) * 5 + x)

operator new[](sizeof(T) * 5 + x, std::align_val_t(alignof(T)))

Here, each instance of x is a non-negative unspecified value representing array allocation overhead; the result of the new-expression will be offset by this amount from the value returned by operator new[]. This overhead may be applied in all array new-expressions, including those referencing the library function operator new[](std​::​size_­t, void*) and other placement allocation functions. The amount of overhead may vary from one invocation of new to another.

However, in my opinion, this x offset cannot be arbitrary. If it is not a multiple of alignment, then the new expression would return a non-aligned pointer (in all cases. Not just the zero, but the non-zero size parameter as well). That's clearly not we want.

So I think this is a hole in the standard. Value of x should be constrained to be a multiple of alignment (at least in the non-zero allocation case). But because of this omission, it seems that the standard doesn't guarantee that a new[] expression returns an aligned pointer at all (in the non-zero case as well).

like image 139
geza Avatar answered Sep 24 '22 01:09

geza