Alignment of array with 0 elements
Asked Answered
A

1

21

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?

Auld answered 16/12, 2017 at 8:53 Comment(2)
As dereferencing the pointer is UB does it matter?Cas
@RichardCritten I honestly don't know. That's part of why I'm asking.Auld
Q
8

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).

Quinte answered 16/12, 2017 at 12:41 Comment(7)
sadly, this does not answer the question either; first, the result of the new expression may be arbitrarily offset from the allocation result in the array case (see [expr.new#15]) so it does not prove that the result of the new expression is aligned for the zero-sized array case.Greenland
Second, it's not clear if a non-aligned pointer used to represent an empty array is "suitably aligned so that it can be converted to a pointer..." or not ... the only thing we know (from [basic.compound#3]) is that a non-aligned pointer is an invalid pointer value, but nowhere is said that delete[] requires a valid pointer value, it's just said that it needs the result of a previous new[]...Greenland
@MassimilianoJanes: For your first concern: why zero-sized arrays special here? Second: if invalid pointer value was allowed, then that sentence wouldn't make any sense. I think that "can be converted" means that it won't end up in an invalid value.Quinte
void* operator new[](std::size_t size) only needs to return a pointer that is "suitably aligned to represent any array object of that size or smaller".Unhinge
"why zero-sized arrays special here?" in the non-zero case it's required to point the the first array element (so, if the allocation for the alignement of T is supported, the pointer must be aligned), whereas in the zero case we only know that the result of the expression "new T[0]" is "new[](x) + offset x" where x is an unspecified non negative number.Greenland
@MassimilianoJanes: it seems that you have a valid point on this one. However, something is strange to me. That x offset should be a multiple of alignment, shouldn't it (in the general case)? If it is not, then the new expression would return a pointer with bad alignment. But of course, an implementation could choose that x can be anything for the 0-size case. I'll edit my answer a little bit, thanks for the info!Quinte
I think the whole issue boils down to the question: should the new expression and the allocation functions required to always result in a valid pointer value (even in the zero array case) ? if yes, [basic.compound#3] applies and non-null valid pointers must be always aligned (indipendently of the object they point to, if any). As you, I'm inclined on the positive in both cases, but I don't know ... :)Greenland

© 2022 - 2024 — McMap. All rights reserved.