new A[0]: Initialization of zero-size array with dynamic storage duration for a class with inaccessible constructor
Asked Answered
U

2

13

I have a C++ concept for checking that an array of objects can be dynamically allocated:

template< class T, int N >
concept heap_constructible = requires() {
    delete[] new T[N];
};

And I found accidently that compilers diverge in its evaluation in case of zero-size arrays N=0 and inaccessible (e.g. private:) constructor:

class A {
    A();
};

static_assert( !heap_constructible<A, 5> ); // OK everywhere
static_assert( heap_constructible<A, 0> );  // OK in GCC only

Only GCC seems to allow zero-size allocation of A-objects. Clang prints the error:

calling a private constructor of class 'A'

In case of slight modification of the concept:

template< class T, int N >
concept heap_constructible1 = requires() {
    delete[] new T[N]{}; // note additional empty braced list
};

Clang accepts it as well:

static_assert( heap_constructible1<A, 0> ); // OK in GCC and Clang

but not MSVC, which evaluates both heap_constructible<A, 0> and heap_constructible1<A, 0> to false. Online demo: https://gcc.godbolt.org/z/nYr88avM4

Which compiler is right here?

Unconformity answered 22/5, 2023 at 15:48 Comment(18)
Standard C and C++ do not allow zero-length arrays. GCC and G++ allow them as an extension.Datcha
@JohnBollinger No, new char[0] is valid standard C++ while char arr[0]; is not.Pappano
@JohnBollinger See C++ new int[0] -- will it allocate memory?Pappano
@Jason, new char[0] does not declare a zero-length array. But you're right that my previous comment is not directly relevant to the question.Datcha
Looks like ill-formed no diagnostic required to me.Pappano
Here is a reduced example. So the concept part can be removed from the question.Pappano
@Jason ... and changing it to auto ptr = new A[0]{}; makes clang ok with it too, like OP observed.Mikkel
Standard verbiage aside, I think it's safe to say GCC's behavior makes the most sense here.Bullington
@TedLyngmo Yes, so concept part isn't needed in the question to see the observed behavior.Pappano
@Jason Probably not. I meant the comment as a confirmation of your observation anyway.Mikkel
@TedLyngmo I see. Btw, upon reading more, I think the program is well-formed as the standard says: "allocation function is called to allocate an array with no elements." So the intention seems that when there isn't any element in the array, there is no need of any initialization and so no need for the constructor to be public. Though the standard should probably clear this up by adding some more explanation into the document i.e clarify if this should be the interpretation.Pappano
@Jason Yes, unless there's a paragraph explaining why adding {} makes it ok but leaving it out is not, gcc seems to be correct like StoryTeller said.Mikkel
Syntactically the N in new T[N] is not a constant-expression or some other context in which a constant expression is required. Furthermore, I don't see any special casing for when the expression in the brackets is a (converted) constant expression in [expr.new]. Therefore, I don't see how the necessity for overload resolution or accessibility checks can depend on the actual value of the expression N.Nonstriated
@Nonstriated I don't think anyone's said that N needs to be a constant expression in new T[N] - or I may have missed it. Whom are you addressing?Mikkel
@TedLyngmo Yes, exactly. For that reason a syntactical check that happens at compile-time shouldn't depend on the value of the expression. I am addressing nobody in particular here. I just felt it wasn't formal enough for an answer.Nonstriated
@Nonstriated Aha, I see what you mean. Good point and thanks for clearing that up!Mikkel
I believe it is only fair to mention to SO users that you also opened a bug report on this issue and it would be helpful in the bug report to specify that you also asked the question on SO. This allows for better information sharing.Cormack
@ShafikYaghmour, thanks, indeed. I just added a reference to this discussion in LLVM bug report.Unconformity
Y
4

Use of constructors by a new-expression is currently underspecified; this is CWG2102 (originating from this Clang issue).


The array bound in a new-expression does not have to be a constant expression, and when it is not, there's no way to tell until runtime whether the new-initializer covers every element of the array or if extra initialization will need to be done for the trailing elements. This means that, in general, the element type needs to be default-constructible.

However, when the array bound is a constant expression, this requirement seems superfluous. And indeed, all major implementations accept code like #1 in the following example (while rejecting #2):

struct S {
    S() = delete;
    S(int);
};

const int a = 3;
S* p = new S[a] {1, 2, 3}; // #1

int b = 3;
S* q = new S[b] {1, 2, 3}; // #2

But the standard does not make a distinction between the two cases.


With that in mind, I'd say that GCC's behavior is the most consistent here: neither default- ([dcl.init.general]/7.2) nor aggregate ([dcl.init.aggr]) initialization of a zero-sized array of T use any of T's constructors, so if new S[3] {1, 2, 3} in the above example is OK, new S[0] should also be fine.

Yolande answered 22/5, 2023 at 19:21 Comment(0)
P
2

I think that gcc is correct in accepting the program here according to the most sensible interpretation of the wording in expr.new:

When the value of the expression is zero, the allocation function is called to allocate an array with no elements.

(emphasis mine)

The intention of the above clause seems to be that since the array has no elements, there is no need for any initialization and hence no need for the constructor to be accessible or even exist.


I also think this clause in the standard could be made more clear by adding something like "In this case there is no need for the ctor to be accessible...."

Pappano answered 22/5, 2023 at 16:56 Comment(5)
What's the application?! Is the result nullptr or pointer to uninitialized memory? Both cases lead to UB. Either the return from underlying function is never freed or the returned pointer will be used as a proper object.Leucoma
@Leucoma It will not return nullptr in C++ and there's no UB if it's not dereferenced. A motivation for why it's possible can be found in https://mcmap.net/q/107931/-c-new-int-0-will-it-allocate-memory "The intent is to have operator new() implementable by calling malloc() or calloc(), so the rules are substantially the same. C++ differs from C in requiring a zero request to return a non-null pointer."Mikkel
The N is not required to be a constant expression or is special cased for constant expressions. So at best you could say that it is unspecified whether or not the constructor overload resolution and accessibility check are done. Nothing I see in [expr.new] is requiring the compiler to diagnose whether the size is a constant expression.Nonstriated
@TedLyngmo Addressing the answer's author here. The answer suggest that the compiler ought to not check the overload resolution and accessibility in this case, but that would require the compiler to test that the runtime expression is a constant expression, which I do not see as reasonable if nothing in the standard's wording specifically makes that distinction.Nonstriated
@Nonstriated Oups, I removed my comment before you answered :) ... but thanks for this too.Mikkel

© 2022 - 2024 — McMap. All rights reserved.