Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Clang] Fix __is_array returning true for zero-sized arrays #86652

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

philnik777
Copy link
Contributor

Fixes #54705

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Mar 26, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Mar 26, 2024

@llvm/pr-subscribers-clang

Author: Nikolas Klauser (philnik777)

Changes

Fixes #54705


Full diff: https://github.com/llvm/llvm-project/pull/86652.diff

3 Files Affected:

  • (modified) clang/docs/ReleaseNotes.rst (+2)
  • (modified) clang/lib/Sema/SemaExprCXX.cpp (+4)
  • (modified) clang/test/SemaCXX/type-traits.cpp (+3-1)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 7fbe2fec6ca065..e6682028537101 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -345,6 +345,8 @@ Bug Fixes in This Version
 - Fixes an assertion failure on invalid code when trying to define member
   functions in lambdas.
 
+- ``__is_array`` no longer returns ``true`` for zero-sized arrays. Fixes (#GH54705).
+
 Bug Fixes to Compiler Builtins
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp
index c34a40fa7c81ac..93d2b4b259fbc3 100644
--- a/clang/lib/Sema/SemaExprCXX.cpp
+++ b/clang/lib/Sema/SemaExprCXX.cpp
@@ -5143,6 +5143,10 @@ static bool EvaluateUnaryTypeTrait(Sema &Self, TypeTrait UTT,
   case UTT_IsFloatingPoint:
     return T->isFloatingType();
   case UTT_IsArray:
+    // zero-sized arrays aren't considered arrays in partial specializations,
+    // so __is_array shouldn't consider them arrays either.
+    if (const auto* CAT = C.getAsConstantArrayType(T))
+      return CAT->getSize() != 0;
     return T->isArrayType();
   case UTT_IsBoundedArray:
     if (!T->isVariableArrayType()) {
diff --git a/clang/test/SemaCXX/type-traits.cpp b/clang/test/SemaCXX/type-traits.cpp
index 14ec17989ec7c7..49df4b668513c0 100644
--- a/clang/test/SemaCXX/type-traits.cpp
+++ b/clang/test/SemaCXX/type-traits.cpp
@@ -25,6 +25,7 @@ typedef Empty EmptyArMB[1][2];
 typedef int Int;
 typedef Int IntAr[10];
 typedef Int IntArNB[];
+typedef Int IntArZero[0];
 class Statics { static int priv; static NonPOD np; };
 union EmptyUnion {};
 union IncompleteUnion; // expected-note {{forward declaration of 'IncompleteUnion'}}
@@ -685,6 +686,7 @@ void is_array()
 {
   static_assert(__is_array(IntAr));
   static_assert(__is_array(IntArNB));
+  static_assert(!__is_array(IntArZero));
   static_assert(__is_array(UnionAr));
 
   static_assert(!__is_array(void));
@@ -1804,7 +1806,7 @@ void is_layout_compatible(int n)
   static_assert(!__is_layout_compatible(EnumForward, int));
   static_assert(!__is_layout_compatible(EnumClassForward, int));
   // FIXME: the following should be rejected (array of unknown bound and void are the only allowed incomplete types)
-  static_assert(__is_layout_compatible(CStructIncomplete, CStructIncomplete)); 
+  static_assert(__is_layout_compatible(CStructIncomplete, CStructIncomplete));
   static_assert(!__is_layout_compatible(CStruct, CStructIncomplete));
   static_assert(__is_layout_compatible(CStructIncomplete[2], CStructIncomplete[2]));
 }

Copy link

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff 5b544b511c7133fcb26a5c563b746a4baefb38d6 90f88bdac3dc40635c58f3f67e29354e6a32896e -- clang/lib/Sema/SemaExprCXX.cpp clang/test/SemaCXX/type-traits.cpp
View the diff from clang-format here.
diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp
index 93d2b4b259..46dd14a871 100644
--- a/clang/lib/Sema/SemaExprCXX.cpp
+++ b/clang/lib/Sema/SemaExprCXX.cpp
@@ -5145,7 +5145,7 @@ static bool EvaluateUnaryTypeTrait(Sema &Self, TypeTrait UTT,
   case UTT_IsArray:
     // zero-sized arrays aren't considered arrays in partial specializations,
     // so __is_array shouldn't consider them arrays either.
-    if (const auto* CAT = C.getAsConstantArrayType(T))
+    if (const auto *CAT = C.getAsConstantArrayType(T))
       return CAT->getSize() != 0;
     return T->isArrayType();
   case UTT_IsBoundedArray:

Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My primary question is: then what is it?

We return true for __is_aggregrate (https://godbolt.org/z/67zjeo7Mj), and an aggregate is an array or class type (https://eel.is/c++draft/dcl.init.aggr#1). This isn't a class type... but it is an aggregate... so it must be an array? (We also don't claim it's a pointer or a reference currently... so this thing will be basically invisible to type traits.)

I would think it is an array given that it uses array syntax for declarations and array semantics for accesses, but it's also not an array that's usable so I can see why others say it's not an array. Personally, I think Clang's behavior here makes the most sense -- we claim it's an array, we also claim it's a bounded array, and we claim its extent is zero (https://godbolt.org/z/4GdYTh4GG), and that matches the declaration for the type. So with this change, I'm worried about how type traits can possibly reason about this type -- I'd like to understand better why other implementations decided this isn't an array and what it's classified as with their type traits. Assuming that logic is compelling, there's more work needed here to handle things like claiming it's a bounded array but not an array.

Also, do we need an ABI tag for folks to get the old behavior given that this change almost certainly will impact ABI through type traits?

@@ -1804,7 +1806,7 @@ void is_layout_compatible(int n)
static_assert(!__is_layout_compatible(EnumForward, int));
static_assert(!__is_layout_compatible(EnumClassForward, int));
// FIXME: the following should be rejected (array of unknown bound and void are the only allowed incomplete types)
static_assert(__is_layout_compatible(CStructIncomplete, CStructIncomplete));
static_assert(__is_layout_compatible(CStructIncomplete, CStructIncomplete));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spurious whitespace change.

@philnik777
Copy link
Contributor Author

philnik777 commented Mar 26, 2024

My primary question is: then what is it?

We return true for __is_aggregrate (https://godbolt.org/z/67zjeo7Mj), and an aggregate is an array or class type (https://eel.is/c++draft/dcl.init.aggr#1). This isn't a class type... but it is an aggregate... so it must be an array? (We also don't claim it's a pointer or a reference currently... so this thing will be basically invisible to type traits.)

According to the spec it's ill-formed, so I'm not sure it falls under any sensible category.

I would think it is an array given that it uses array syntax for declarations and array semantics for accesses, but it's also not an array that's usable so I can see why others say it's not an array. Personally, I think Clang's behavior here makes the most sense -- we claim it's an array, we also claim it's a bounded array, and we claim its extent is zero (https://godbolt.org/z/4GdYTh4GG), and that matches the declaration for the type. So with this change, I'm worried about how type traits can possibly reason about this type -- I'd like to understand better why other implementations decided this isn't an array and what it's classified as with their type traits. Assuming that logic is compelling, there's more work needed here to handle things like claiming it's a bounded array but not an array.

For MSVC it's probably because they don't support T[0]. The main reason I don't think it should be considered an array for the type traits is that it doesn't behave like other arrays. e.g. the implementation that's used without the builtin is

template <class T>
inline constexpr bool is_array_v = false;

template <class T>
inline constexpr bool is_array_v<T[]> = true;

template <class T, size_t Size>
inline constexpr bool is_array_v<T[Size]> = true;

For T[0] this returns false.

Another example, with a type trait that returns true:

template <class T, unsigned Size>
void takes_an_array(const T(& arr)[Size]) {}

template <class T>
void do_stuff(T&& v) {
  if constexpr (__is_array(__remove_cvref(T))) {
    takes_an_array(v);
  }
}

void call() {
  int arr[0] = {};
  do_stuff(arr);
}

This fails to compile because takes_an_array doesn't match.

Also, do we need an ABI tag for folks to get the old behavior given that this change almost certainly will impact ABI through type traits?

We don't use the trait currently in libc++ because of this bug and GCC only added it in trunk (and have the same bug), so probably not.

@AaronBallman
Copy link
Collaborator

According to the spec it's ill-formed, so I'm not sure it falls under any sensible category.

It's an extension we support so it's up to us to decide what sensible is.

For T[0] this returns false.

Understood, but why is this not the bug to be fixed? (It would also fix the takes_an_array case as well.)

We don't use the trait currently in libc++ because of this bug and GCC only added it in trunk (and have the same bug), so probably not.

For example, it seems to be used in Unreal Engine: https://github.com/windystrife/UnrealEngine_NVIDIAGameWorks/blob/b50e6338a7c5b26374d66306ebc7807541ff815e/Engine/Source/Runtime/Core/Public/Templates/UnrealTemplate.h#L128

@philnik777
Copy link
Contributor Author

According to the spec it's ill-formed, so I'm not sure it falls under any sensible category.

It's an extension we support so it's up to us to decide what sensible is.

Sure. If we end up putting it in the array category we should probably coordinate with GCC.

For T[0] this returns false.

Understood, but why is this not the bug to be fixed? (It would also fix the takes_an_array case as well.)

Because I don't think we can. AFAICT this is valid C++:

template <unsigned Size, class = int[Size]>
int func() {
  return 1;
}

template <unsigned Size>
int func() {
  return 0;
}

If clang accepted int[0] in this context the observable behaviour would change.

We don't use the trait currently in libc++ because of this bug and GCC only added it in trunk (and have the same bug), so probably not.

For example, it seems to be used in Unreal Engine: https://github.com/windystrife/UnrealEngine_NVIDIAGameWorks/blob/b50e6338a7c5b26374d66306ebc7807541ff815e/Engine/Source/Runtime/Core/Public/Templates/UnrealTemplate.h#L128

If you think it's a significant enough change that we should add an ABI tag I can do that.

@AaronBallman
Copy link
Collaborator

According to the spec it's ill-formed, so I'm not sure it falls under any sensible category.

It's an extension we support so it's up to us to decide what sensible is.

Sure. If we end up putting it in the array category we should probably coordinate with GCC.

For T[0] this returns false.

Understood, but why is this not the bug to be fixed? (It would also fix the takes_an_array case as well.)

Because I don't think we can. AFAICT this is valid C++:

template <unsigned Size, class = int[Size]>
int func() {
  return 1;
}

template <unsigned Size>
int func() {
  return 0;
}

If clang accepted int[0] in this context the observable behaviour would change.

Hmm, I think you're right: https://godbolt.org/z/PjGoc3Yob, the static_assert(func<0>() == 0); case would start to fail to compile, similar to the code using func<1>().

This doesn't leave us with good options, does it? :-(

I think we need to consider the following things:

  • Should Ty[0] and Ty[] be handled the same way? (Remember, we support [0] in structures as a sort of flexible array member and we support flexible array members in C++ as an extension.)
  • We should probably change the value of __is_bounded_array at the same time as __is_array.
  • What should the behavior of __is_aggregate be?
  • What about other type traits (__is_compound, __is_abstract, etc)? Basically, what's the principle we're using for this type?

We don't use the trait currently in libc++ because of this bug and GCC only added it in trunk (and have the same bug), so probably not.

For example, it seems to be used in Unreal Engine: https://github.com/windystrife/UnrealEngine_NVIDIAGameWorks/blob/b50e6338a7c5b26374d66306ebc7807541ff815e/Engine/Source/Runtime/Core/Public/Templates/UnrealTemplate.h#L128

If you think it's a significant enough change that we should add an ABI tag I can do that.

I don't have a good feeling one way or the other yet, so let's hold off for the moment and see what the final scope of the changes are before making a decision.

@philnik777
Copy link
Contributor Author

This doesn't leave us with good options, does it? :-(

Not really. Zero-sized arrays should probably have been supported by C++ all along, but it's too late to change it now.

I think we need to consider the following things:

* Should `Ty[0]` and `Ty[]` be handled the same way? (Remember, we support `[0]` in structures as a sort of flexible array member and we support flexible array members in C++ as an extension.)

I'm not sure what you're referring to. AFAICT T[] at the end of a struct denotes a flexible array member, but T[0] doesn't. Or does it?

* We should probably change the value of `__is_bounded_array` at the same time as `__is_array`.

Yes. I'll update the PR accordingly.

* What should the behavior of `__is_aggregate` be?

I guess it should be true? I can't think of any case where a zero-sized array would be different from a non-zero-sized one at least. I don't have a strong opinion either way though.

* What about other type traits (`__is_compound`, `__is_abstract`, etc)? Basically, what's the principle we're using for this type?

Maybe we could think of it as array-like? i.e. it has the same traits as an array except that it's not one. I think the semantics only really break down for array-specific features. Just to get a sense, I've gone through a few traits:

  • is_compound: it seems pretty clear to me that it is a compound type, since T[0] is a distinct type from T, but contains a type T in its definition.
  • is_class: It seems pretty clear to me that it's not a class. I don't think anybody would disagree here.
  • is_abstract: You can define a variable of it, so it's not. It's also not a class type.
  • is_object: I think it is, but this could be argued either way. The best thing I could come up with is that it's closer to an array than it is to a function or reference type.

is_empty seems like an interesting case. It's the smallest type there is, with sizeof(int[0]) == 0, but it's not considered empty by any implementation. MSVC rejects the sizeof call though. I think it shouldn't be considered empty though, since is_empty kind-of implies that the type is a class type.

We don't use the trait currently in libc++ because of this bug and GCC only added it in trunk (and have the same bug), so probably not.

For example, it seems to be used in Unreal Engine: https://github.com/windystrife/UnrealEngine_NVIDIAGameWorks/blob/b50e6338a7c5b26374d66306ebc7807541ff815e/Engine/Source/Runtime/Core/Public/Templates/UnrealTemplate.h#L128

If you think it's a significant enough change that we should add an ABI tag I can do that.

I don't have a good feeling one way or the other yet, so let's hold off for the moment and see what the final scope of the changes are before making a decision.

Sounds good.

@AaronBallman
Copy link
Collaborator

I think we need to consider the following things:

* Should `Ty[0]` and `Ty[]` be handled the same way? (Remember, we support `[0]` in structures as a sort of flexible array member and we support flexible array members in C++ as an extension.)

I'm not sure what you're referring to. AFAICT T[] at the end of a struct denotes a flexible array member, but T[0] doesn't. Or does it?

It doesn't per spec but it does as a common compiler extension: https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html

So my question is more that, if T[0] isn't considered an array because it has no size, should T[] be considered the same way?

* We should probably change the value of `__is_bounded_array` at the same time as `__is_array`.

Yes. I'll update the PR accordingly.

* What should the behavior of `__is_aggregate` be?

I guess it should be true? I can't think of any case where a zero-sized array would be different from a non-zero-sized one at least. I don't have a strong opinion either way though.

I'm not entirely certain of how this particular type trait is used in practice. Based on https://sourcegraph.com/search?q=context:global+lang:C%2B%2B+std::is_aggregate+-file:.*libcxx.*+-file:.*test.*+-file:.*clang.*+-file:.*gcc.*&patternType=keyword&sm=0 it seems that most uses for this trait are to decide whether you can use aggregate initialization or not, and Ty[0] can be "used" in empty aggregate initialization (e.g., int x[0] = {};) so perhaps returning true makes sense? But then again, an aggregate is a class or array type, so the fact that Ty[0] will be reported as false for both of those doesn't seem ideal. CC @ldionne @mordante @cor3ntin for more opinions

* What about other type traits (`__is_compound`, `__is_abstract`, etc)? Basically, what's the principle we're using for this type?

Maybe we could think of it as array-like? i.e. it has the same traits as an array except that it's not one. I think the semantics only really break down for array-specific features. Just to get a sense, I've gone through a few traits:

* `is_compound`: it seems pretty clear to me that it is a compound type, since `T[0]` is a distinct type from `T`, but contains a type `T` in its definition.

* `is_class`: It seems pretty clear to me that it's not a class. I don't think anybody would disagree here.

* `is_abstract`: You can define a variable of it, so it's not. It's also not a class type.

* `is_object`: I think it is, but this could be argued either way. The best thing I could come up with is that it's closer to an array than it is to a function or reference type.

is_empty seems like an interesting case. It's the smallest type there is, with sizeof(int[0]) == 0, but it's not considered empty by any implementation. MSVC rejects the sizeof call though. I think it shouldn't be considered empty though, since is_empty kind-of implies that the type is a class type.

My natural inclination is that it is array-like, but... that just makes me want __is_array to return true for it all the more. That's... what it is. I wonder if we should be considering making zero-length arrays into a non-conforming extension behind a flag? e.g., -fzero-sized-arrays and then it does report true for __is_array, __is_bounded_array, and handles template specializations, etc as though it were really an array. That solves two problems: 1) we can make the feature regular as opposed to having so many sharp edges, 2) it pushes a surprising and questionable extension behind an opt-in feature flag, and it creates at least one problem: 1) potentially would change behavior of existing code in C++. So maybe this isn't a tenable idea, but do we think it's worth exploring?

Oh wow, and it gets even more surprising... adding a zero-length array to a structure reduces the size of the structure (in C++): https://godbolt.org/z/5E4YsT1Ye

CC @nickdesaulniers @kees @bwendling for more opinions from people who live with this kind of extension...

@kees
Copy link
Contributor

kees commented Mar 28, 2024

My natural inclination is that it is array-like, but... that just makes me want __is_array to return true for it all the more.

Yes. An array is an array, regardless of its size. The size is just a storage characteristic. It'd almost be like arguing that NaN isn't a float.

I wonder if we should be considering making zero-length arrays into a non-conforming extension behind a flag?

It was never as simple as zero-length arrays. It was also trailing arrays of any size in structures. Those extensions create huge problems with being able to reason about the characteristics of arrays, and we do have a flag for this: -fstrict-flex-arrays=3. It is designed to disable all of the "treat it like a flexible array" logic for the old "fake flexible array" objects. But a flexible array is still an array. And also note that its size can change at runtime (see the counted_by attribute), which even more clearly argues that you can't claim a flexible array isn't an array.

@philnik777
Copy link
Contributor Author

I wonder if we should be considering making zero-length arrays into a non-conforming extension behind a flag? e.g., -fzero-sized-arrays and then it does report true for __is_array, __is_bounded_array, and handles template specializations, etc as though it were really an array. That solves two problems: 1) we can make the feature regular as opposed to having so many sharp edges, 2) it pushes a surprising and questionable extension behind an opt-in feature flag, and it creates at least one problem: 1) potentially would change behavior of existing code in C++. So maybe this isn't a tenable idea, but do we think it's worth exploring?

I think it may be worth exploring, but I'm not sure it's feasible. We've been using them accidentally a lot in the test suite in libc++ simply because it's really easy to miss that it's an extension, and we're also using it in our string implementation for padding.

My natural inclination is that it is array-like, but... that just makes me want __is_array to return true for it all the more.

Yes. An array is an array, regardless of its size. The size is just a storage characteristic. It'd almost be like arguing that NaN isn't a float.

I don't think the float analogy really works here. The fact is that zero-sized arrays aren't acknowledged as being valid by the standard, so they don't behave like other arrays. It's not just a storage characteristic. Having -ffast-math and then passing NAN somewhere seems like a better analogy to me. Things are mostly treated as if NAN didn't exist, and NAN also doesn't work like other floats.

I wonder if we should be considering making zero-length arrays into a non-conforming extension behind a flag?

It was never as simple as zero-length arrays. It was also trailing arrays of any size in structures. Those extensions create huge problems with being able to reason about the characteristics of arrays, and we do have a flag for this: -fstrict-flex-arrays=3. It is designed to disable all of the "treat it like a flexible array" logic for the old "fake flexible array" objects. But a flexible array is still an array. And also note that its size can change at runtime (see the counted_by attribute), which even more clearly argues that you can't claim a flexible array isn't an array.

I'm not sure how the array being able to change its size at runtime makes it harder to claim it isn't one? non-zero-sized arrays don't do that. It's actually another thing that is different from the rest of the arrays.
I'm also not sure that it makes a ton of sense to talk about flexible arrays, since they aren't part of the type system, but T[0] happen to become flexible arrays if it is at the end of a struct (IIUC).

Another option (that I haven't thought deeply about): Make T[0] not be a type at all, i.e. make it illegal to decltype, sizeof etc. uses of it. I don't know how breaking that would be, and it's probably just as confusing as the current state of things, or even more so. Just wanted to throw it out.

@philnik777
Copy link
Contributor Author

Another option would be to to not acknowledge zero-sized arrays in the type traits at all, just like vector types: https://godbolt.org/z/aP685vz8q. That may be the most consistent stance on this. Basically "It's non-standard and doesn't fit neatly in any of the standard categories, so it's in no category at all."

@kees
Copy link
Contributor

kees commented Mar 29, 2024

I guess I don't have a strong opinion here, since these helpers are specific to C++, and I've been generally trying to remove fixed-size 0-sized arrays in C projects (i.e. the Linux kernel). I do care about C flexible arrays (and their associated extensions), though. I suspect there is some existing weirdness for C++ when using static initializers, though. See https://godbolt.org/z/PnYMcxhfM

struct S { int a; int b[]; };
static struct S s = { .a = 42, .b = { 10, 20, 30, 40 }, };
static struct S z = { .a = 13, };

Specifically z.b is a 0-sized flexible array, but s.b isn't. Should they have different results?

@AaronBallman
Copy link
Collaborator

Another option would be to to not acknowledge zero-sized arrays in the type traits at all, just like vector types: https://godbolt.org/z/aP685vz8q. That may be the most consistent stance on this. Basically "It's non-standard and doesn't fit neatly in any of the standard categories, so it's in no category at all."

That's a possibility, though I wonder if it's actually user-friendly to issue a diagnostic if trying to use a non-standard type that can't fit the C++ type traits model. It's annoying for folks writing generic code, but so long as the diagnostic is SFINAE-able, maybe it's okay? My concern is that either returning true or false from any of those traits may give really surprising results to users, whereas a diagnostic makes it clear "you've wandered off the beaten path here and you need to think more carefully about what this all means". It's not quite as far as the idea of making them invalid types and perhaps is a bad idea, but I'm also somewhat okay with the idea of making it harder to use these non-standard extensions in highly generic code.

@jwakely
Copy link
Contributor

jwakely commented Mar 31, 2024

We don't use the trait currently in libc++ because of this bug and GCC only added it in trunk (and have the same bug), so probably not.

N.B. MSVC used to have the same bug as well, and it affected their std::is_array because MSVC auto-replaces std::is_array<T>::value with the intrinsic (and never instantiates the class template). I noted this behaviour in Feb 2022 and they fixed the intrinsic.

@jwakely
Copy link
Contributor

jwakely commented Mar 31, 2024

As I've just commented at https://gcc.gnu.org/bugzilla/show_bug.cgi?id=114479 the __is_array intrinsic exists to optimize std::is_array and so should match its existing behaviour. If you want to change the behaviour of std::is_array then please do so via an LWG issue, e.g. requesting clarification of the behaviour for T[0].

An optimization to speed up compilation of std::is_array should not dictate its behaviour.

@AaronBallman
Copy link
Collaborator

As I've just commented at https://gcc.gnu.org/bugzilla/show_bug.cgi?id=114479 the __is_array intrinsic exists to optimize std::is_array and so should match its existing behaviour. If you want to change the behaviour of std::is_array then please do so via an LWG issue, e.g. requesting clarification of the behaviour for T[0].

An optimization to speed up compilation of std::is_array should not dictate its behaviour.

Errr, I think there's some circular logic here perhaps:

[dcl.array] says that for T[N] the value "N specifies the array bound, i.e., the
number of elements in the array; N shall be greater than zero."

So T[0] is not a valid array type.

Except T[0] is a valid array type because it's supported as a language extension in Clang and GCC; that's the whole reason you can spell T[0] at all.

This boils down to a fundamental question: what makes something an array type in the first place? Is it the declaration syntax or is it the semantics? If it's declaration syntax, then T[0] is clearly an array type. If it's semantics, then T[0] is not an array type but std::vector<int> is. Personally, I think the declaration syntax is what makes something an array type per https://eel.is/c++draft/dcl.array#1 saying "a declaration of the form". Yes, it says the size shall not be zero, but that's the bit we've extended, so it's circular to say "the standard says zero isn't allowed and therefore the library behavior is to return false from std::is_array".

So I think I'm arguing for an LWG issue.

@jwakely
Copy link
Contributor

jwakely commented Apr 1, 2024

Last time I checked, neither GCC nor Clang allowed T[0] to match a partial specialization for is_array<T[N]> so the extension isn't very well extended :)

@AaronBallman
Copy link
Collaborator

Last time I checked, neither GCC nor Clang allowed T[0] to match a partial specialization for is_array<T[N]> so the extension isn't very well extended :)

Oh, I agree, I am learning to loathe this extension the more I look into this. Did you see this lovely bit:

Oh wow, and it gets even more surprising... adding a zero-length array to a structure reduces the size of the structure (in C++): https://godbolt.org/z/5E4YsT1Ye

But that's why I'm asking questions about what we should be doing with the feature overall. As it currently stands, this extension seems like it is broken in C++ in multiple ways. Fixing a tiny corner of it is fine, but I'd like to know what the overall big picture is so we can know whether the tiny corner changes fit or not. We could fix this by changing partial specializations to care about zero-sized arrays, too. (Personally, I would love to "fix" this by deprecating the extension as the only justification for it I've been able to dig up is as a hack for flexible arrays -- deprecating this extension outside of GNU89 mode has some appeal, barring other information about its value.)

@philnik777
Copy link
Contributor Author

I'd personally be fine with deprecating it. As I said above, we use it in libc++ for padding inside string like

struct short_string {
  <some data>;
  char padding[sizeof(value_type) - 1];
  <more data>;
};

That could be refactored relatively easily as

template <size_t Size>
struct padding_t {
  char data[Size];
};

template <>
struct padding_t<0> {};

struct short_string {
  <some data>;
  [[no_unique_address]] padding_t<sizeof(value_type) - 1> padding;
  <more data>;
};

so I don't think that's a use-case worth keeping the extension for.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Clang reports that __is_array(T[0]) is true
5 participants