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

[libc++] Fix constexpr initialization of std::array<T, 0> #74667

Merged
merged 12 commits into from
Dec 15, 2023

Conversation

ldionne
Copy link
Member

@ldionne ldionne commented Dec 6, 2023

This patch fixes constexpr default initialization of empty arrays and improves the tests accordingly.

Fixes #74375

This patch fixes constexpr default initialization of empty arrays
and improves the tests accordingly.

Fixes llvm#74375
@ldionne ldionne requested a review from a team as a code owner December 6, 2023 22:36
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Dec 6, 2023
@llvmbot
Copy link
Collaborator

llvmbot commented Dec 6, 2023

@llvm/pr-subscribers-libcxx

Author: Louis Dionne (ldionne)

Changes

This patch fixes constexpr default initialization of empty arrays and improves the tests accordingly.

Fixes #74375


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

4 Files Affected:

  • (modified) libcxx/include/array (+2-1)
  • (modified) libcxx/test/std/containers/sequences/array/array.cons/initialization.pass.cpp (+21-4)
  • (added) libcxx/test/std/containers/sequences/array/size_and_alignment.compile.pass.cpp (+130)
  • (removed) libcxx/test/std/containers/sequences/array/size_and_alignment.pass.cpp (-78)
diff --git a/libcxx/include/array b/libcxx/include/array
index 127092f6bca9b..334fa747339ba 100644
--- a/libcxx/include/array
+++ b/libcxx/include/array
@@ -280,7 +280,8 @@ struct _LIBCPP_TEMPLATE_VIS array<_Tp, 0>
     typedef std::reverse_iterator<iterator>       reverse_iterator;
     typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
 
-    typedef __conditional_t<is_const<_Tp>::value, const char, char> _CharType;
+    struct _EmptyAggregate { };
+    typedef __conditional_t<is_const<_Tp>::value, const _EmptyAggregate, _EmptyAggregate> _CharType;
 
     struct  _ArrayInStructT { _Tp __data_[1]; };
     _ALIGNAS_TYPE(_ArrayInStructT) _CharType __elems_[sizeof(_ArrayInStructT)];
diff --git a/libcxx/test/std/containers/sequences/array/array.cons/initialization.pass.cpp b/libcxx/test/std/containers/sequences/array/array.cons/initialization.pass.cpp
index 9153106b384fc..bdc79e22013f8 100644
--- a/libcxx/test/std/containers/sequences/array/array.cons/initialization.pass.cpp
+++ b/libcxx/test/std/containers/sequences/array/array.cons/initialization.pass.cpp
@@ -19,11 +19,9 @@ struct NoDefault {
 };
 
 // Test default initialization
-// This one isn't constexpr because omitting to initialize fundamental types
-// isn't valid in a constexpr context.
 struct test_default_initialization {
     template <typename T>
-    void operator()() const
+    TEST_CONSTEXPR_CXX14 void operator()() const
     {
         std::array<T, 0> a0; (void)a0;
         std::array<T, 1> a1; (void)a1;
@@ -34,6 +32,20 @@ struct test_default_initialization {
     }
 };
 
+// Additional tests for constexpr default initialization of empty arrays since
+// it seems that making a variable constexpr and merely having a non-constexpr
+// variable in a constexpr function behave differently.
+//
+// Reproducer for https://github.com/llvm/llvm-project/issues/74375
+struct test_default_initialization_74375 {
+    template <typename T>
+    TEST_CONSTEXPR_CXX14 void operator()() const
+    {
+        constexpr std::array<T, 0> a0; (void)a0;
+        constexpr std::array<NoDefault, 0> nodefault; (void)nodefault;
+    }
+};
+
 struct test_nondefault_initialization {
     template <typename T>
     TEST_CONSTEXPR_CXX14 void operator()() const
@@ -177,10 +189,15 @@ TEST_CONSTEXPR_CXX14 bool with_all_types()
 int main(int, char**)
 {
     with_all_types<test_nondefault_initialization>();
-    with_all_types<test_default_initialization>(); // not constexpr
+    with_all_types<test_default_initialization>();
+    with_all_types<test_default_initialization_74375>();
     test_initializer_list();
 #if TEST_STD_VER >= 14
     static_assert(with_all_types<test_nondefault_initialization>(), "");
+#if TEST_STD_VER >= 20
+    static_assert(with_all_types<test_default_initialization>(), "");
+#endif
+    static_assert(with_all_types<test_default_initialization_74375>(), "");
     static_assert(test_initializer_list(), "");
 #endif
 
diff --git a/libcxx/test/std/containers/sequences/array/size_and_alignment.compile.pass.cpp b/libcxx/test/std/containers/sequences/array/size_and_alignment.compile.pass.cpp
new file mode 100644
index 0000000000000..66069715103fd
--- /dev/null
+++ b/libcxx/test/std/containers/sequences/array/size_and_alignment.compile.pass.cpp
@@ -0,0 +1,130 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// <array>
+
+// template <class T, size_t N>
+// struct array
+
+// Make sure std::array<T, N> has the correct object size and alignment.
+// This test is mostly meant to catch subtle ABI-breaking regressions.
+
+// Ignore error about requesting a large alignment not being ABI compatible with older AIX systems.
+#if defined(_AIX)
+#  pragma clang diagnostic ignored "-Waix-compat"
+#endif
+
+#include <array>
+#include <cstddef>
+
+#include "test_macros.h"
+
+template <class T, std::size_t Size>
+struct MyArray {
+  T elems[Size];
+};
+
+template <class T>
+void test_type() {
+  {
+    static_assert(sizeof(std::array<T, 0>) == sizeof(T), "");
+    static_assert(alignof(std::array<T, 0>) == alignof(T), "");
+    static_assert(sizeof(std::array<T, 0>) == sizeof(T[1]), "");
+    static_assert(sizeof(std::array<T, 0>) == sizeof(MyArray<T, 1>), "");
+    static_assert(alignof(std::array<T, 0>) == alignof(MyArray<T, 1>), "");
+  }
+
+  {
+    static_assert(sizeof(std::array<T, 1>) == sizeof(T), "");
+    static_assert(alignof(std::array<T, 1>) == alignof(T), "");
+    static_assert(sizeof(std::array<T, 1>) == sizeof(T[1]), "");
+    static_assert(sizeof(std::array<T, 1>) == sizeof(MyArray<T, 1>), "");
+    static_assert(alignof(std::array<T, 1>) == alignof(MyArray<T, 1>), "");
+  }
+
+  {
+    static_assert(sizeof(std::array<T, 2>) == sizeof(T) * 2, "");
+    static_assert(alignof(std::array<T, 2>) == alignof(T), "");
+    static_assert(sizeof(std::array<T, 2>) == sizeof(T[2]), "");
+    static_assert(sizeof(std::array<T, 2>) == sizeof(MyArray<T, 2>), "");
+    static_assert(alignof(std::array<T, 2>) == alignof(MyArray<T, 2>), "");
+  }
+
+  {
+    static_assert(sizeof(std::array<T, 3>) == sizeof(T) * 3, "");
+    static_assert(alignof(std::array<T, 3>) == alignof(T), "");
+    static_assert(sizeof(std::array<T, 3>) == sizeof(T[3]), "");
+    static_assert(sizeof(std::array<T, 3>) == sizeof(MyArray<T, 3>), "");
+    static_assert(alignof(std::array<T, 3>) == alignof(MyArray<T, 3>), "");
+  }
+
+  {
+    static_assert(sizeof(std::array<T, 444>) == sizeof(T) * 444, "");
+    static_assert(alignof(std::array<T, 444>) == alignof(T), "");
+    static_assert(sizeof(std::array<T, 444>) == sizeof(T[444]), "");
+    static_assert(sizeof(std::array<T, 444>) == sizeof(MyArray<T, 444>), "");
+    static_assert(alignof(std::array<T, 444>) == alignof(MyArray<T, 444>), "");
+  }
+}
+
+struct Empty {};
+
+struct Aggregate {
+  int i;
+};
+
+struct WithPadding {
+  long double ld;
+  char c;
+};
+
+#if TEST_STD_VER >= 11
+struct alignas(alignof(std::max_align_t) * 2) Overaligned1 {};
+
+struct alignas(alignof(std::max_align_t) * 2) Overaligned2 {
+  char data[1000];
+};
+
+struct alignas(alignof(std::max_align_t)) Overaligned3 {
+  char data[1000];
+};
+
+struct alignas(8) Overaligned4 {
+  char c;
+};
+
+struct alignas(8) Overaligned5 {};
+#endif
+
+int main(int, char**) {
+  test_type<char>();
+  test_type<short>();
+  test_type<int>();
+  test_type<long>();
+  test_type<long long>();
+  test_type<float>();
+  test_type<double>();
+  test_type<long double>();
+  test_type<char[1]>();
+  test_type<char[2]>();
+  test_type<char[3]>();
+  test_type<Empty>();
+  test_type<Aggregate>();
+  test_type<WithPadding>();
+
+#if TEST_STD_VER >= 11
+  test_type<std::max_align_t>();
+  test_type<Overaligned1>();
+  test_type<Overaligned2>();
+  test_type<Overaligned3>();
+  test_type<Overaligned4>();
+  test_type<Overaligned5>();
+#endif
+
+  return 0;
+}
diff --git a/libcxx/test/std/containers/sequences/array/size_and_alignment.pass.cpp b/libcxx/test/std/containers/sequences/array/size_and_alignment.pass.cpp
deleted file mode 100644
index 6fbc844a11eac..0000000000000
--- a/libcxx/test/std/containers/sequences/array/size_and_alignment.pass.cpp
+++ /dev/null
@@ -1,78 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
-// See https://llvm.org/LICENSE.txt for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-//===----------------------------------------------------------------------===//
-
-// <array>
-
-// template <class T, size_t N>
-// struct array
-
-// Test the size and alignment matches that of an array of a given type.
-
-// Ignore error about requesting a large alignment not being ABI compatible with older AIX systems.
-#if defined(_AIX)
-# pragma clang diagnostic ignored "-Waix-compat"
-#endif
-
-#include <array>
-#include <iterator>
-#include <type_traits>
-#include <cstddef>
-
-#include "test_macros.h"
-
-template <class T, std::size_t Size>
-struct MyArray {
-  T elems[Size];
-};
-
-template <class T, std::size_t Size>
-void test() {
-  typedef T CArrayT[Size == 0 ? 1 : Size];
-  typedef std::array<T, Size> ArrayT;
-  typedef MyArray<T, Size == 0 ? 1 : Size> MyArrayT;
-  static_assert(sizeof(ArrayT) == sizeof(CArrayT), "");
-  static_assert(sizeof(ArrayT) == sizeof(MyArrayT), "");
-  static_assert(TEST_ALIGNOF(ArrayT) == TEST_ALIGNOF(MyArrayT), "");
-}
-
-template <class T>
-void test_type() {
-  test<T, 1>();
-  test<T, 42>();
-  test<T, 0>();
-}
-
-#if TEST_STD_VER >= 11
-struct alignas(alignof(std::max_align_t) * 2) TestType1 {
-
-};
-
-struct alignas(alignof(std::max_align_t) * 2) TestType2 {
-  char data[1000];
-};
-
-struct alignas(alignof(std::max_align_t)) TestType3 {
-  char data[1000];
-};
-#endif
-
-int main(int, char**) {
-  test_type<char>();
-  test_type<int>();
-  test_type<double>();
-  test_type<long double>();
-
-#if TEST_STD_VER >= 11
-  test_type<std::max_align_t>();
-  test_type<TestType1>();
-  test_type<TestType2>();
-  test_type<TestType3>();
-#endif
-
-  return 0;
-}

Copy link

github-actions bot commented Dec 6, 2023

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

You can test this locally with the following command:
git-clang-format --diff 1f283a60a4bb896fa2d37ce00a3018924be82b9f 61b538dcf3da75b52e80767b569e37b798028e2b -- libcxx/test/std/containers/sequences/array/size_and_alignment.compile.pass.cpp libcxx/include/array libcxx/test/std/concepts/concepts.lang/concept.default.init/default_initializable.compile.pass.cpp libcxx/test/std/containers/sequences/array/array.cons/initialization.pass.cpp libcxx/test/std/containers/views/mdspan/extents/CtorTestCombinations.h
View the diff from clang-format here.
diff --git a/libcxx/test/std/concepts/concepts.lang/concept.default.init/default_initializable.compile.pass.cpp b/libcxx/test/std/concepts/concepts.lang/concept.default.init/default_initializable.compile.pass.cpp
index 786c57ebfd..4ef44545e4 100644
--- a/libcxx/test/std/concepts/concepts.lang/concept.default.init/default_initializable.compile.pass.cpp
+++ b/libcxx/test/std/concepts/concepts.lang/concept.default.init/default_initializable.compile.pass.cpp
@@ -199,7 +199,7 @@ void test()
     test_not_const<void(Empty::*)(const int&) noexcept(false)>();
 
     // Sequence containers
-    test_true     <std::array<               int, 0>>();
+    test_true<std::array< int, 0>>();
     test_not_const<std::array<               int, 1>>();
     test_false    <std::array<const          int, 1>>();
     test_not_const<std::array<      volatile int, 1>>();
diff --git a/libcxx/test/std/containers/sequences/array/array.cons/initialization.pass.cpp b/libcxx/test/std/containers/sequences/array/array.cons/initialization.pass.cpp
index 7991d4738d..bbc1b3346c 100644
--- a/libcxx/test/std/containers/sequences/array/array.cons/initialization.pass.cpp
+++ b/libcxx/test/std/containers/sequences/array/array.cons/initialization.pass.cpp
@@ -19,56 +19,73 @@ struct NoDefault {
 };
 
 struct test_initialization {
-    template <typename T>
-    TEST_CONSTEXPR_CXX14 void operator()() const
+  template <typename T>
+  TEST_CONSTEXPR_CXX14 void operator()() const {
+    // Check default initalization
     {
-        // Check default initalization
-        {
-            std::array<T, 0> a0; (void)a0;
-            // Before C++20, default initialization doesn't work inside constexpr for
-            // trivially default constructible types. This only apply to non-empty arrays,
-            // since empty arrays don't hold an element of type T.
-            if (TEST_STD_AT_LEAST_20_OR_RUNTIME_EVALUATED || !std::is_trivially_default_constructible<T>::value) {
-                std::array<T, 1> a1; (void)a1;
-                std::array<T, 2> a2; (void)a2;
-                std::array<T, 3> a3; (void)a3;
-            }
-
-            std::array<NoDefault, 0> nodefault; (void)nodefault;
-        }
-
-        // A const empty array can also be default-initialized regardless of the type
-        // it contains. For non-empty arrays, this doesn't work whenever T doesn't
-        // have a user-provided default constructor.
-        {
-            const std::array<T, 0> a0; (void)a0;
-            const std::array<NoDefault, 0> nodefault; (void)nodefault;
-        }
+      std::array<T, 0> a0;
+      (void)a0;
+      // Before C++20, default initialization doesn't work inside constexpr for
+      // trivially default constructible types. This only apply to non-empty arrays,
+      // since empty arrays don't hold an element of type T.
+      if (TEST_STD_AT_LEAST_20_OR_RUNTIME_EVALUATED || !std::is_trivially_default_constructible<T>::value) {
+        std::array<T, 1> a1;
+        (void)a1;
+        std::array<T, 2> a2;
+        (void)a2;
+        std::array<T, 3> a3;
+        (void)a3;
+      }
+
+      std::array<NoDefault, 0> nodefault;
+      (void)nodefault;
+    }
 
-        // Check direct-list-initialization syntax (introduced in C++11)
-    #if TEST_STD_VER >= 11
-        {
-            {
-                std::array<T, 0> a0_0{}; (void)a0_0;
-            }
-            {
-                std::array<T, 1> a1_0{}; (void)a1_0;
-                std::array<T, 1> a1_1{T()}; (void)a1_1;
-            }
-            {
-                std::array<T, 2> a2_0{}; (void)a2_0;
-                std::array<T, 2> a2_1{T()}; (void)a2_1;
-                std::array<T, 2> a2_2{T(), T()}; (void)a2_2;
-            }
-            {
-                std::array<T, 3> a3_0{}; (void)a3_0;
-                std::array<T, 3> a3_1{T()}; (void)a3_1;
-                std::array<T, 3> a3_2{T(), T()}; (void)a3_2;
-                std::array<T, 3> a3_3{T(), T(), T()}; (void)a3_3;
-            }
+    // A const empty array can also be default-initialized regardless of the type
+    // it contains. For non-empty arrays, this doesn't work whenever T doesn't
+    // have a user-provided default constructor.
+    {
+      const std::array<T, 0> a0;
+      (void)a0;
+      const std::array<NoDefault, 0> nodefault;
+      (void)nodefault;
+    }
 
-            std::array<NoDefault, 0> nodefault{}; (void)nodefault;
-        }
+    // Check direct-list-initialization syntax (introduced in C++11)
+#if TEST_STD_VER >= 11
+    {
+      {
+        std::array<T, 0> a0_0{};
+        (void)a0_0;
+      }
+      {
+        std::array<T, 1> a1_0{};
+        (void)a1_0;
+        std::array<T, 1> a1_1{T()};
+        (void)a1_1;
+      }
+      {
+        std::array<T, 2> a2_0{};
+        (void)a2_0;
+        std::array<T, 2> a2_1{T()};
+        (void)a2_1;
+        std::array<T, 2> a2_2{T(), T()};
+        (void)a2_2;
+      }
+      {
+        std::array<T, 3> a3_0{};
+        (void)a3_0;
+        std::array<T, 3> a3_1{T()};
+        (void)a3_1;
+        std::array<T, 3> a3_2{T(), T()};
+        (void)a3_2;
+        std::array<T, 3> a3_3{T(), T(), T()};
+        (void)a3_3;
+      }
+
+      std::array<NoDefault, 0> nodefault{};
+      (void)nodefault;
+    }
     #endif
 
         // Check copy-list-initialization syntax
@@ -119,7 +136,7 @@ struct test_initialization {
             // See http://wg21.link/LWG2157
             std::array<NoDefault, 0> nodefault = {{}}; (void)nodefault;
         }
-    }
+  }
 };
 
 // Test construction from an initializer-list
@@ -187,21 +204,21 @@ TEST_CONSTEXPR_CXX14 bool with_all_types()
 #if TEST_STD_VER >= 20
 template <class T>
 concept is_list_initializable_int = requires {
-    { T{123} };
+  { T{123} };
 };
 
-struct Foo { };
+struct Foo {};
 static_assert(!is_list_initializable_int<std::array<Foo, 0>>);
 static_assert(!is_list_initializable_int<std::array<Foo, 1>>);
 #endif
 
 int main(int, char**)
 {
-    with_all_types<test_initialization>();
-    test_initializer_list();
+  with_all_types<test_initialization>();
+  test_initializer_list();
 #if TEST_STD_VER >= 14
-    static_assert(with_all_types<test_initialization>(), "");
-    static_assert(test_initializer_list(), "");
+  static_assert(with_all_types<test_initialization>(), "");
+  static_assert(test_initializer_list(), "");
 #endif
 
     return 0;

Copy link
Member

@mordante mordante left a comment

Choose a reason for hiding this comment

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

LGTM modulo some nits.

@@ -280,7 +280,8 @@ struct _LIBCPP_TEMPLATE_VIS array<_Tp, 0>
typedef std::reverse_iterator<iterator> reverse_iterator;
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;

typedef __conditional_t<is_const<_Tp>::value, const char, char> _CharType;
struct _EmptyAggregate { };
typedef __conditional_t<is_const<_Tp>::value, const _EmptyAggregate, _EmptyAggregate> _CharType;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
typedef __conditional_t<is_const<_Tp>::value, const _EmptyAggregate, _EmptyAggregate> _CharType;
using _EmptyType = __maybe_const<_Tp>::value, _EmptyAggregate>;

// #include <__type_traits/maybe_const.h> is needed too
The name was not great before, but now it looks really weird since there is no connection with char at all.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree with renaming to _EmptyType, that was an oversight. However, I'd like to keep the __maybe_const out of this change, since it's really unrelated.

Copy link
Contributor

@philnik777 philnik777 left a comment

Choose a reason for hiding this comment

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

I'd really like to see a test that makes sure the new struct isn't considered empty and there are no padding bytes.

libcxx/include/array Outdated Show resolved Hide resolved
Comment on lines 37 to 42
static_assert(sizeof(Array) == sizeof(T), "");
static_assert(TEST_ALIGNOF(Array) == TEST_ALIGNOF(T), "");
static_assert(sizeof(Array) == sizeof(T[1]), "");
static_assert(sizeof(Array) == sizeof(MyArray<T, 1>), "");
static_assert(TEST_ALIGNOF(Array) == TEST_ALIGNOF(MyArray<T, 1>), "");
static_assert(!std::is_empty<Array>::value, "");
Copy link
Contributor

Choose a reason for hiding this comment

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

These look to me like they're all implementation-dependent. Am I missing some requirement, or should we make them libc++-specific?

Copy link
Member Author

Choose a reason for hiding this comment

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

cppreference says:

This container is an aggregate type with the same semantics as a struct holding a C-style array T[N] as its only non-static data member.

I think it's reasonable (and helpful for other implementations) to have these checks enabled for all implementations.

The tests for the 0-sized specialization could potentially be made libc++ only, but then again I think this will help enforce that all implementations provide compatible implementations.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was only talking about the zero-sized ones. The reality is that implementations already disagree on most things tested here: https://godbolt.org/z/75hqP4Yx6

Copy link
Member Author

Choose a reason for hiding this comment

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

I see, I thought implementations agreed on this a bit more than they actually do.

@EricWF
Copy link
Member

EricWF commented Dec 14, 2023

Hmm, So I see we already migrated away from not returning nullptr from array<T, 0>. My understanding was that was explicitly prohibited by the standard when it says

2
#
In the case that N == 0, begin() == end() == unique value. The return value of data() is unspecified.

The "unique" bit specifically saying that the two arrays can't both return nullptr.

I think we may have strayed from the right implementation here, and done it a while ago. I would like to consider this more.

Copy link
Member

@EricWF EricWF left a comment

Choose a reason for hiding this comment

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

OK, I've done my due diligence offline, and this seems like a not incorrect fix.

Though as a general note, I think our tests have become too clever.

// Before C++20, default initialization doesn't work inside constexpr for
// trivially default constructible types. This only apply to non-empty arrays,
// since empty arrays don't hold an element of type T.
if (TEST_STD_AT_LEAST_20_OR_RUNTIME_EVALUATED || !std::is_trivially_default_constructible<T>::value) {
Copy link
Member

Choose a reason for hiding this comment

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

I really hate logic in tests. The point where we have this many conditionals is the point where we should probably make the tests less generic and just spam the test out multiple times.

Copy link
Member Author

Choose a reason for hiding this comment

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

I know -- I started like that and it was unreadable since I could hardly reuse anything at all, and I'm spamming exactly the same test over and over again for a bunch of different types. I dislike logic in tests, but I think this strikes the right balance after spending a bunch of time investigating different approaches.

// This one isn't constexpr because omitting to initialize fundamental types
// isn't valid in a constexpr context.
struct test_default_initialization {
struct test_initialization {
Copy link
Member

Choose a reason for hiding this comment

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

Please make these separate tests rather than trying to reuse the instantiation. It's just clearer for readers.

Copy link
Member Author

Choose a reason for hiding this comment

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

The only reason this was multiple structs previously is basically that I couldn't run the default initialization test in constexpr, so I needed another named struct for it. The way this is written right now follows our usual pattern of having something like:

constexpr bool test() {
  // test case 1
  {
    ...
  }
  // test case 2
  {
    ...
  }
  // test case 3
  {
    ...
  }

  return true;
}

int main() {
  test();
  static_assert(test());
}

// template <class T, size_t N>
// struct array

// Make sure std::array<T, N> has the correct object size and alignment.
Copy link
Member

Choose a reason for hiding this comment

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

Only partially related but...

Any chance we've changed the triviality of any of the special members with this change?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a great observation! This is tested in https://github.com/llvm/llvm-project/blob/main/libcxx/test/libcxx/containers/sequences/array/triviality.pass.cpp and I think this already has the appropriate coverage, but I am happy to add to it if you think we're missing something there.

@ldionne
Copy link
Member Author

ldionne commented Dec 14, 2023

Hmm, So I see we already migrated away from not returning nullptr from array<T, 0>. My understanding was that was explicitly prohibited by the standard when it says

2 # In the case that N == 0, begin() == end() == unique value. The return value of data() is unspecified.

The "unique" bit specifically saying that the two arrays can't both return nullptr.

I think we may have strayed from the right implementation here, and done it a while ago. I would like to consider this more.

Just to be clear though, this patch doesn't affect whether we return nullptr or not from begin(), nor our ability to do so. It only fixes cases where we should be able to initialize an empty array inside constexpr and we currently can't, without breaking the ABI. If you wanted to return nullptr from begin() (which I think you can't without contradicting the standard), you could in theory do it orthogonally to this patch.

Edit: It looks like we raced on this!
Edit2: My comment is actually completely wrong. We are returning nullptr from begin() since 7265ff9. And yes I agree with you that this is technically not standards-conforming, however that commit explains why all other implementations are also not standards-conforming.

@rnicholl-google
Copy link

Hmm, So I see we already migrated away from not returning nullptr from array<T, 0>. My understanding was that was explicitly prohibited by the standard when it says
2 # In the case that N == 0, begin() == end() == unique value. The return value of data() is unspecified.
The "unique" bit specifically saying that the two arrays can't both return nullptr.
I think we may have strayed from the right implementation here, and done it a while ago. I would like to consider this more.

Just to be clear though, this patch doesn't affect whether we return nullptr or not from begin(), nor our ability to do so. It only fixes cases where we should be able to initialize an empty array inside constexpr and we currently can't, without breaking the ABI. If you wanted to return nullptr from begin() (which I think you can't without contradicting the standard), you could in theory do it orthogonally to this patch.

Edit: It looks like we raced on this! Edit2: My comment is actually completely wrong. We are returning nullptr from begin() since 7265ff9. And yes I agree with you that this is technically not standards-conforming, however that commit explains why all other implementations are also not standards-conforming.

I may have found a possible way to do a standard compliant implementation, so don't merge this yet, I'll try to have a patch within the week. I'm not sure if it'll work fully yet and pass all tests but I found a hack with unions that might make the non-nullptr version implementable.

@rnicholl-google
Copy link

Update: My fix doesn't work because of a different clang bug, so go ahead and merge this if it works, I think I can separately merge the other one if the other bug is fixed.

@ldionne
Copy link
Member Author

ldionne commented Dec 15, 2023

I will merge this since it passes CI and looks like folks are satisfied. @EricWF if you want to talk about how I wrote the tests, I'm happy to go over it and change them in a future patch once we agree.

@ldionne ldionne merged commit 70bcd81 into llvm:main Dec 15, 2023
42 of 44 checks passed
@ldionne ldionne deleted the review/array-constexpr-init branch December 15, 2023 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[libc++] Initialization of std::array of size 0 in constexpr contexts
6 participants