Skip to content

[libc++] Slightly simplify max_size and add new tests for vector #119990

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

Merged
merged 2 commits into from
Feb 6, 2025

Conversation

winner245
Copy link
Contributor

@winner245 winner245 commented Dec 14, 2024

This PR slightly simplifies the implementation of vector<bool>::max_size and adds extensive tests for the max_size() function for both vector<bool> and vector<T>. The main purposes of the new tests include:

  • Verify correctness of max_size() under various size_type and difference_type definitions: check that max_size() works properly with allocators that have custom size_type and difference_type. This is particularly useful for vector<bool>, as different size_type lead to different __storage_type of different word lengths, resulting in varying max_size() values for vector<bool>. Additionally, different difference_type also sets different upper limit of max_size() for both vector<bool> and std::vector. These tests were previously missing.

  • Eliminate incorrect implementations: Special tests are added to identify and reject incorrect implementations of vector<bool>::max_size that unconditionally return std::min<size_type>(__nmax, __internal_cap_to_external(__amax)). This can cause overflow in the __internal_cap_to_external() call and lead to incorrect results. The new tests ensure that such incorrect implementations are identified.

Copy link

github-actions bot commented Dec 15, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

@winner245 winner245 force-pushed the simplify-max-size branch 3 times, most recently from e3b7708 to f08d58c Compare December 16, 2024 11:26
@winner245 winner245 marked this pull request as ready for review December 16, 2024 16:32
@winner245 winner245 requested a review from a team as a code owner December 16, 2024 16:32
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Dec 16, 2024
@llvmbot
Copy link
Member

llvmbot commented Dec 16, 2024

@llvm/pr-subscribers-libcxx

Author: Peng Liu (winner245)

Changes

This PR slightly simplifies the implementation of vector&lt;bool&gt;::max_size and adds extensive tests for the max_size() function for both vector<bool> and std::vector. The main purposes of the new tests include:

  • Verify correctness of max_size() under various size_type and difference_type definitions: check that max_size() works properly with allocators that have custom size_type and difference_type. This is particularly useful for vector&lt;bool&gt;, as different size_type lead to different __storage_type of different word lengths, resulting in varying max_size() values for vector&lt;bool&gt;. Additionally, different difference_type also sets different upper limit of max_size() for both vector&lt;bool&gt; and std::vector. These tests were previously missing.

  • Eliminate incorrect implementations: Special tests are added to identify and reject incorrect implementations of vector&lt;bool&gt;::max_size that unconditionally return std::min&lt;size_type&gt;(__nmax, __internal_cap_to_external(__amax)). This can cause overflow in the __internal_cap_to_external() call and lead to incorrect results. The new tests ensure that such incorrect implementations are identified.


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

4 Files Affected:

  • (modified) libcxx/include/__vector/vector_bool.h (+2-4)
  • (added) libcxx/test/std/containers/sequences/vector.bool/max_size.pass.cpp (+105)
  • (modified) libcxx/test/std/containers/sequences/vector/vector.capacity/max_size.pass.cpp (+45-7)
  • (added) libcxx/test/support/sized_allocator.h (+58)
diff --git a/libcxx/include/__vector/vector_bool.h b/libcxx/include/__vector/vector_bool.h
index 36eb7f350ac406..84e0167e71a6cb 100644
--- a/libcxx/include/__vector/vector_bool.h
+++ b/libcxx/include/__vector/vector_bool.h
@@ -511,10 +511,8 @@ template <class _Allocator>
 _LIBCPP_CONSTEXPR_SINCE_CXX20 typename vector<bool, _Allocator>::size_type
 vector<bool, _Allocator>::max_size() const _NOEXCEPT {
   size_type __amax = __storage_traits::max_size(__alloc_);
-  size_type __nmax = numeric_limits<size_type>::max() / 2; // end() >= begin(), always
-  if (__nmax / __bits_per_word <= __amax)
-    return __nmax;
-  return __internal_cap_to_external(__amax);
+  size_type __nmax = numeric_limits<difference_type>::max();
+  return __nmax / __bits_per_word <= __amax ? __nmax : __internal_cap_to_external(__amax);
 }
 
 //  Precondition:  __new_size > capacity()
diff --git a/libcxx/test/std/containers/sequences/vector.bool/max_size.pass.cpp b/libcxx/test/std/containers/sequences/vector.bool/max_size.pass.cpp
new file mode 100644
index 00000000000000..84cf2a7de539e0
--- /dev/null
+++ b/libcxx/test/std/containers/sequences/vector.bool/max_size.pass.cpp
@@ -0,0 +1,105 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// <vector>
+
+// size_type max_size() const;
+
+#include <cassert>
+#include <climits>
+#include <cstdint>
+#include <limits>
+#include <memory>
+#include <type_traits>
+#include <vector>
+
+#include "min_allocator.h"
+#include "sized_allocator.h"
+#include "test_allocator.h"
+#include "test_macros.h"
+
+#if TEST_STD_VER >= 11
+
+template <typename Alloc>
+TEST_CONSTEXPR_CXX20 void test(const std::vector<bool, Alloc>& v) {
+  using Vector              = std::vector<bool, Alloc>;
+  using size_type           = typename Vector::size_type;
+  using difference_type     = typename Vector::difference_type;
+  using storage_type        = typename Vector::__storage_type;
+  using storage_alloc       = typename std::allocator_traits<Alloc>::template rebind_alloc<storage_type>;
+  using storage_traits      = typename std::allocator_traits<Alloc>::template rebind_traits<storage_type>;
+  const size_type max_dist  = static_cast<size_type>(std::numeric_limits<difference_type>::max());
+  const size_type max_alloc = storage_traits::max_size(storage_alloc(v.get_allocator()));
+  std::size_t bits_per_word = sizeof(storage_type) * CHAR_BIT;
+  const size_type max_size  = max_dist / bits_per_word < max_alloc ? max_dist : max_alloc * bits_per_word;
+  assert(v.max_size() <= max_dist);
+  assert(v.max_size() / bits_per_word <= max_alloc); // max_alloc * bits_per_word may overflow
+  LIBCPP_ASSERT(v.max_size() == max_size);
+}
+
+#endif
+
+TEST_CONSTEXPR_CXX20 bool tests() {
+  // Test with limited_allocator where v.max_size() is determined by allocator::max_size()
+  {
+    using Alloc        = limited_allocator<bool, 10>;
+    using Vector       = std::vector<bool, Alloc>;
+    using storage_type = Vector::__storage_type;
+    Vector v;
+    std::size_t bits_per_word = sizeof(storage_type) * CHAR_BIT;
+    assert(v.max_size() <= 10 * bits_per_word);
+    LIBCPP_ASSERT(v.max_size() == 10 * bits_per_word);
+  }
+  {
+    using Alloc        = limited_allocator<double, 10>;
+    using Vector       = std::vector<bool, Alloc>;
+    using storage_type = Vector::__storage_type;
+    Vector v;
+    std::size_t bits_per_word = sizeof(storage_type) * CHAR_BIT;
+    assert(v.max_size() <= 10 * bits_per_word);
+    LIBCPP_ASSERT(v.max_size() == 10 * bits_per_word);
+  }
+
+#if TEST_STD_VER >= 11
+
+  // Test with various allocators and diffrent size_type
+  {
+    test(std::vector<bool>());
+    test(std::vector<bool, std::allocator<int> >());
+    test(std::vector<bool, min_allocator<bool> >());
+    test(std::vector<bool, test_allocator<bool> >(test_allocator<bool>(1)));
+    test(std::vector<bool, other_allocator<bool> >(other_allocator<bool>(5)));
+    test(std::vector<bool, sized_allocator<bool, std::uint8_t, std::int8_t> >());
+    test(std::vector<bool, sized_allocator<bool, std::uint16_t, std::int16_t> >());
+    test(std::vector<bool, sized_allocator<bool, std::uint32_t, std::int32_t> >());
+    test(std::vector<bool, sized_allocator<bool, std::uint64_t, std::int64_t> >());
+    test(std::vector<bool, limited_allocator<bool, static_cast<std::size_t>(-1)> >());
+  }
+
+  // Test cases to identify incorrect implementations that unconditionally return:
+  //   std::min<size_type>(__nmax, __internal_cap_to_external(__amax))
+  // This can cause overflow in __internal_cap_to_external and lead to incorrect results.
+  {
+    test(std::vector<bool, limited_allocator<bool, static_cast<std::size_t>(-1) / 61> >());
+    test(std::vector<bool, limited_allocator<bool, static_cast<std::size_t>(-1) / 63> >());
+  }
+
+#endif
+
+  return true;
+}
+
+int main(int, char**) {
+  tests();
+
+#if TEST_STD_VER >= 20
+  static_assert(tests());
+#endif
+
+  return 0;
+}
diff --git a/libcxx/test/std/containers/sequences/vector/vector.capacity/max_size.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.capacity/max_size.pass.cpp
index 33abaa04b12079..bc4d4e3fc24ad2 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.capacity/max_size.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.capacity/max_size.pass.cpp
@@ -11,15 +11,34 @@
 // size_type max_size() const;
 
 #include <cassert>
+#include <cstdint>
 #include <limits>
 #include <type_traits>
 #include <vector>
 
+#include "min_allocator.h"
+#include "sized_allocator.h"
 #include "test_allocator.h"
 #include "test_macros.h"
 
+#if TEST_STD_VER >= 11
 
-TEST_CONSTEXPR_CXX20 bool test() {
+template <typename T, typename Alloc>
+TEST_CONSTEXPR_CXX20 void test(const std::vector<T, Alloc>& v) {
+  using Vector              = std::vector<T, Alloc>;
+  using alloc_traits        = typename Vector::__alloc_traits;
+  using size_type           = typename Vector::size_type;
+  using difference_type     = typename Vector::difference_type;
+  const size_type max_dist  = static_cast<size_type>(std::numeric_limits<difference_type>::max());
+  const size_type max_alloc = alloc_traits::max_size(v.get_allocator());
+  assert(v.max_size() <= max_dist);
+  assert(v.max_size() <= max_alloc);
+  LIBCPP_ASSERT(v.max_size() == std::min<size_type>(max_dist, max_alloc));
+}
+
+#endif
+
+TEST_CONSTEXPR_CXX20 bool tests() {
   {
     typedef limited_allocator<int, 10> A;
     typedef std::vector<int, A> C;
@@ -30,29 +49,48 @@ TEST_CONSTEXPR_CXX20 bool test() {
   {
     typedef limited_allocator<int, (std::size_t)-1> A;
     typedef std::vector<int, A> C;
-    const C::size_type max_dist =
-        static_cast<C::size_type>(std::numeric_limits<C::difference_type>::max());
+    const C::size_type max_dist = static_cast<C::size_type>(std::numeric_limits<C::difference_type>::max());
     C c;
     assert(c.max_size() <= max_dist);
     LIBCPP_ASSERT(c.max_size() == max_dist);
   }
   {
     typedef std::vector<char> C;
-    const C::size_type max_dist =
-        static_cast<C::size_type>(std::numeric_limits<C::difference_type>::max());
+    const C::size_type max_dist = static_cast<C::size_type>(std::numeric_limits<C::difference_type>::max());
     C c;
     assert(c.max_size() <= max_dist);
     assert(c.max_size() <= alloc_max_size(c.get_allocator()));
+    LIBCPP_ASSERT(c.max_size() == std::min(max_dist, alloc_max_size(c.get_allocator())));
+  }
+
+#if TEST_STD_VER >= 11
+
+  // Test with various allocators and diffrent size_type
+  {
+    test(std::vector<int>());
+    test(std::vector<short, std::allocator<short> >());
+    test(std::vector<unsigned, min_allocator<unsigned> >());
+    test(std::vector<char, test_allocator<char> >(test_allocator<char>(1)));
+    test(std::vector<std::size_t, other_allocator<std::size_t> >(other_allocator<std::size_t>(5)));
+    test(std::vector<int, sized_allocator<int, std::uint8_t, std::int8_t> >());
+    test(std::vector<int, sized_allocator<int, std::uint16_t, std::int16_t> >());
+    test(std::vector<int, sized_allocator<int, std::uint32_t, std::int32_t> >());
+    test(std::vector<int, sized_allocator<int, std::uint64_t, std::int64_t> >());
+    test(std::vector<int, limited_allocator<int, static_cast<std::size_t>(-1)> >());
+    test(std::vector<int, limited_allocator<int, static_cast<std::size_t>(-1) / 2> >());
+    test(std::vector<int, limited_allocator<int, static_cast<std::size_t>(-1) / 4> >());
   }
 
+#endif
+
   return true;
 }
 
 int main(int, char**) {
-  test();
+  tests();
 
 #if TEST_STD_VER > 17
-  static_assert(test());
+  static_assert(tests());
 #endif
 
   return 0;
diff --git a/libcxx/test/support/sized_allocator.h b/libcxx/test/support/sized_allocator.h
new file mode 100644
index 00000000000000..a877252e82962c
--- /dev/null
+++ b/libcxx/test/support/sized_allocator.h
@@ -0,0 +1,58 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef TEST_SUPPORT_SIZED_ALLOCATOR_H
+#define TEST_SUPPORT_SIZED_ALLOCATOR_H
+
+#include <cstddef>
+#include <limits>
+#include <memory>
+#include <new>
+
+#include "test_macros.h"
+
+template <typename T, typename SIZE_TYPE = std::size_t, typename DIFF_TYPE = std::ptrdiff_t>
+class sized_allocator {
+  template <typename U, typename Sz, typename Diff>
+  friend class sized_allocator;
+
+public:
+  using value_type                  = T;
+  using size_type                   = SIZE_TYPE;
+  using difference_type             = DIFF_TYPE;
+  using propagate_on_container_swap = std::true_type;
+
+  TEST_CONSTEXPR_CXX20 explicit sized_allocator(int d = 0) : data_(d) {}
+
+  template <typename U, typename Sz, typename Diff>
+  TEST_CONSTEXPR_CXX20 sized_allocator(const sized_allocator<U, Sz, Diff>& a) TEST_NOEXCEPT : data_(a.data_) {}
+
+  TEST_CONSTEXPR_CXX20 T* allocate(size_type n) {
+    if (n > max_size())
+      TEST_THROW(std::bad_array_new_length());
+    return std::allocator<T>().allocate(n);
+  }
+
+  TEST_CONSTEXPR_CXX20 void deallocate(T* p, size_type n) TEST_NOEXCEPT { std::allocator<T>().deallocate(p, n); }
+
+  TEST_CONSTEXPR size_type max_size() const TEST_NOEXCEPT {
+    return std::numeric_limits<size_type>::max() / sizeof(value_type);
+  }
+
+private:
+  int data_;
+
+  TEST_CONSTEXPR friend bool operator==(const sized_allocator& a, const sized_allocator& b) {
+    return a.data_ == b.data_;
+  }
+  TEST_CONSTEXPR friend bool operator!=(const sized_allocator& a, const sized_allocator& b) {
+    return a.data_ != b.data_;
+  }
+};
+
+#endif

@winner245 winner245 force-pushed the simplify-max-size branch 2 times, most recently from babd945 to 6056c7d Compare December 16, 2024 17:09
Copy link
Member

@ldionne ldionne left a comment

Choose a reason for hiding this comment

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

This basically LGTM, but I'd like to see this again with the updated tests before merging. Thanks a lot for the fix!

using Vector = std::vector<bool, Alloc>;
using size_type = typename Vector::size_type;
using difference_type = typename Vector::difference_type;
using storage_type = typename Vector::__storage_type;
Copy link
Member

Choose a reason for hiding this comment

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

This alias only exists in libc++, and our test suite must be portable. I suspect this means you will need to significantly weaken the tests for non-libc++ implementations, and that's okay. But the tests should still work on non-libc++ implementations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've made part of the test libc++ only because libstdc++'s implementation is slightly different from libc++. The return values in the two implementations are, in math form (assuming no overflow)

min(numeric_limits<difference_type>::max() - bits_per_word + 1, alloc_traits::max_size() * bits_per_word); // libstdc++
min(numeric_limits<difference_type>::max(), alloc_traits::max_size() * bits_per_word); // libc++, MSVC-STL

Copy link
Contributor Author

@winner245 winner245 Jan 30, 2025

Choose a reason for hiding this comment

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

Another reason why this test is not fully portable to other standard implementations is that libc++, libstdc++, and MSVC-STL all use different word types for vector<bool> underlying storage:

Compiler Word Storage Type
MSVC unsigned int (fixed)
libstdc++ unsigned long (fixed)
libc++ alloc_traits::size_type (configurable through custom allocator)

@ldionne ldionne added the pending-ci Merging the PR is only pending completion of CI label Feb 5, 2025
@ldionne ldionne merged commit efa287d into llvm:main Feb 6, 2025
81 checks passed
@winner245 winner245 deleted the simplify-max-size branch February 6, 2025 04:00
Icohedron pushed a commit to Icohedron/llvm-project that referenced this pull request Feb 11, 2025
…m#119990)

This PR slightly simplifies the implementation of `vector<bool>::max_size`
and adds extensive tests for the `max_size()` function for both `vector<bool>`
and `vector<T>`. The main purposes of the new tests include:

- Verify correctness of `max_size()` under various `size_type` and
  `difference_type` definitions: check that `max_size()` works properly
  with allocators that have custom `size_type` and `difference_type`. This
  is particularly useful for `vector<bool>`, as different `size_type` lead
  to different `__storage_type` of different word lengths, resulting in
  varying `max_size()` values for `vector<bool>`. Additionally, different
  `difference_type` also sets different upper limit of `max_size()` for
  both `vector<bool>` and `std::vector`. These tests were previously
  missing.

- Eliminate incorrect implementations: Special tests are added to identify and
  reject incorrect implementations of `vector<bool>::max_size` that unconditionally
  return `std::min<size_type>(size-max, __internal_cap_to_external(allocator-max-size))`.
  This can cause overflow in the `__internal_cap_to_external()` call and lead
  to incorrect results. The new tests ensure that such incorrect
  implementations are identified.
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. pending-ci Merging the PR is only pending completion of CI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants