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

[libcxx] improves diagnostics for containers with bad value types #106296

Open
wants to merge 31 commits into
base: main
Choose a base branch
from

Conversation

cjdb
Copy link
Contributor

@cjdb cjdb commented Aug 27, 2024

std::vector<int&> generates nearly 170 lines of diagnostic, most of which are redundant, and it's only helpful if you are already familiar with the rule that containers can't hold reference types. This commit reduces it to only a handful of lines, all of which are dedicated to clearly communicating the problem. It also applies this to all the other containers, and for all non-cv-qualified and non-object types.

These static_asserts are placed at the very top of each container because they short-circuit further instantiation errors, thereby leading a smaller set of diagnostics for the programmer. Placing them in std::allocator (which is the common denominator for all containers) doesn't do the short circuiting, and we thus end up with several unhelpful diagnostics after the helpful one.

This commit only cleans up things that are already diagnosed as compile-time errors. In particular, std::map<int&&, int> should be ill-formed, per [associative.reqmts.general]/p8. libc++ and libstdc++ currently permit that, but not std::map<int&, int>. As such, this commit only adds a static assert for map<K&, V>, and defers diagnosing valid productions that should be ill-formed to a future commit.

@cjdb cjdb requested a review from a team as a code owner August 27, 2024 21:38
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Aug 27, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Aug 27, 2024

@llvm/pr-subscribers-libcxx

Author: Christopher Di Bella (cjdb)

Changes

std::vector&lt;int&amp;&gt; generates nearly 170 lines of diagnostic, most of which are redundant, and it's only helpful if you are already familiar with the rule that containers can't hold reference types. This commit reduces it to only a handful of lines, all of which are dedicated to clearly communicating the problem. It also applies this to all the other containers, and for all non-cv-qualified and non-object types.

These static_asserts are placed at the very top of each container because they short-circuit further instantiation errors, thereby leading a smaller set of diagnostics for the programmer. Placing them in std::allocator (which is the common denominator for all containers) doesn't do the short circuiting, and we thus end up with several unhelpful diagnostics after the helpful one.

This commit only cleans up things that are already ill-formed. In particular, std::map&lt;int&amp;&amp;, int&gt; should be diagnosed per [associative.reqmts.general]/p8. It's a valid production in libc++ and libstdc++ today, but std::map&lt;int&amp;, int&gt; isn't. As such, only lvalue references are diagnosed in this commit, and a future commit can handle things that aren't already rejected by the compiler.


Patch is 72.20 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/106296.diff

27 Files Affected:

  • (modified) libcxx/include/__memory/allocator.h (+13-2)
  • (modified) libcxx/include/array (+9)
  • (modified) libcxx/include/deque (+12)
  • (modified) libcxx/include/forward_list (+133)
  • (modified) libcxx/include/list (+134)
  • (modified) libcxx/include/map (+17)
  • (modified) libcxx/include/set (+18)
  • (modified) libcxx/include/string (+13)
  • (modified) libcxx/include/unordered_map (+17)
  • (modified) libcxx/include/unordered_set (+18)
  • (modified) libcxx/include/vector (+13)
  • (added) libcxx/test/libcxx/containers/associative/map/non_cv_object_types.verify.cpp (+53)
  • (added) libcxx/test/libcxx/containers/associative/multimap/non_cv_object_types.verify.cpp (+53)
  • (added) libcxx/test/libcxx/containers/associative/multiset/non_cv_object_types.verify.cpp (+29)
  • (added) libcxx/test/libcxx/containers/associative/set/non_cv_object_types.verify.cpp (+29)
  • (added) libcxx/test/libcxx/containers/sequences/array/non_cv_objects.verify.cpp (+25)
  • (added) libcxx/test/libcxx/containers/sequences/deque/non_cv_objects.verify.cpp (+29)
  • (added) libcxx/test/libcxx/containers/sequences/forward_list/non_cv_objects.verify.cpp (+32)
  • (added) libcxx/test/libcxx/containers/sequences/list/non_cv_objects.verify.cpp (+29)
  • (added) libcxx/test/libcxx/containers/sequences/vector/non_cv_objects.verify.cpp (+29)
  • (added) libcxx/test/libcxx/containers/strings/basic.string/non_cv_objects.verify.cpp (+32)
  • (added) libcxx/test/libcxx/containers/unord/unord.map/non_cv_object_types.verify.cpp (+46)
  • (added) libcxx/test/libcxx/containers/unord/unord.multimap/non_cv_object_types.verify.cpp (+46)
  • (added) libcxx/test/libcxx/containers/unord/unord.multiset/non_cv_object_types.verify.cpp (+33)
  • (added) libcxx/test/libcxx/containers/unord/unord.set/non_cv_object_types.verify.cpp (+32)
  • (added) libcxx/test/libcxx/memory/allocator_non_cv_objects_only.verify.cpp (+25)
  • (removed) libcxx/test/libcxx/memory/allocator_volatile.verify.cpp (-14)
diff --git a/libcxx/include/__memory/allocator.h b/libcxx/include/__memory/allocator.h
index 0dbdc41d3c3d14..36890f4e1a97d6 100644
--- a/libcxx/include/__memory/allocator.h
+++ b/libcxx/include/__memory/allocator.h
@@ -16,9 +16,12 @@
 #include <__memory/allocator_traits.h>
 #include <__type_traits/is_const.h>
 #include <__type_traits/is_constant_evaluated.h>
+#include <__type_traits/is_function.h>
+#include <__type_traits/is_reference.h>
 #include <__type_traits/is_same.h>
 #include <__type_traits/is_void.h>
 #include <__type_traits/is_volatile.h>
+#include <__type_traits/remove_reference.h>
 #include <__utility/forward.h>
 #include <cstddef>
 #include <new>
@@ -76,8 +79,16 @@ struct __non_trivial_if<true, _Unique> {
 
 template <class _Tp>
 class _LIBCPP_TEMPLATE_VIS allocator : private __non_trivial_if<!is_void<_Tp>::value, allocator<_Tp> > {
-  static_assert(!is_const<_Tp>::value, "std::allocator does not support const types");
-  static_assert(!is_volatile<_Tp>::value, "std::allocator does not support volatile types");
+  static_assert(!is_const<_Tp>::value, "'std::allocator' can only allocate non-const object types");
+  static_assert(!is_volatile<_Tp>::value, "'std::allocator' can only allocate non-volatile object types");
+  static_assert(!is_reference<_Tp>::value || !is_function<typename remove_reference<_Tp>::type>::value,
+                "'std::allocator' can only allocate object types; function references are not objects (consider using "
+                "a function pointer)");
+  static_assert(!is_reference<_Tp>::value,
+                "'std::allocator' can only allocate object types; references are not objects");
+  static_assert(
+      !is_function<_Tp>::value,
+      "'std::allocator' can only allocate object types; functions are not objects (consider using a function pointer)");
 
 public:
   typedef size_t size_type;
diff --git a/libcxx/include/array b/libcxx/include/array
index 4db0cb7bd7e3b5..0476f57a86ac24 100644
--- a/libcxx/include/array
+++ b/libcxx/include/array
@@ -127,11 +127,15 @@ template <size_t I, class T, size_t N> const T&& get(const array<T, N>&&) noexce
 #include <__type_traits/is_array.h>
 #include <__type_traits/is_const.h>
 #include <__type_traits/is_constructible.h>
+#include <__type_traits/is_function.h>
 #include <__type_traits/is_nothrow_constructible.h>
+#include <__type_traits/is_reference.h>
 #include <__type_traits/is_same.h>
 #include <__type_traits/is_swappable.h>
 #include <__type_traits/is_trivially_relocatable.h>
+#include <__type_traits/is_void.h>
 #include <__type_traits/remove_cv.h>
+#include <__type_traits/remove_reference.h>
 #include <__utility/empty.h>
 #include <__utility/integer_sequence.h>
 #include <__utility/move.h>
@@ -167,6 +171,11 @@ _LIBCPP_BEGIN_NAMESPACE_STD
 
 template <class _Tp, size_t _Size>
 struct _LIBCPP_TEMPLATE_VIS array {
+  static_assert(!is_reference<_Tp>::value || !is_function<typename remove_reference<_Tp>::type>::value, "'std::array' can only hold object types; function references are not objects (consider using a function pointer)");
+  static_assert(!is_reference<_Tp>::value, "'std::array' can only hold object types; references are not objects");
+  static_assert(!is_function<_Tp>::value, "'std::array' can only hold object types; functions are not objects (consider using a function pointer)");
+  static_assert(!is_void<_Tp>::value, "'std::array' can only hold object types; 'void' is not an object");
+
   using __trivially_relocatable = __conditional_t<__libcpp_is_trivially_relocatable<_Tp>::value, array, void>;
 
   // types:
diff --git a/libcxx/include/deque b/libcxx/include/deque
index 759de5d3a030a6..df0ddaebc39e92 100644
--- a/libcxx/include/deque
+++ b/libcxx/include/deque
@@ -212,9 +212,15 @@ template <class T, class Allocator, class Predicate>
 #include <__ranges/size.h>
 #include <__split_buffer>
 #include <__type_traits/is_allocator.h>
+#include <__type_traits/is_const.h>
 #include <__type_traits/is_convertible.h>
+#include <__type_traits/is_function.h>
+#include <__type_traits/is_reference.h>
 #include <__type_traits/is_same.h>
 #include <__type_traits/is_swappable.h>
+#include <__type_traits/is_void.h>
+#include <__type_traits/is_volatile.h>
+#include <__type_traits/remove_reference.h>
 #include <__type_traits/type_identity.h>
 #include <__utility/forward.h>
 #include <__utility/move.h>
@@ -468,6 +474,12 @@ const _DiffType __deque_iterator<_ValueType, _Pointer, _Reference, _MapPointer,
 
 template <class _Tp, class _Allocator /*= allocator<_Tp>*/>
 class _LIBCPP_TEMPLATE_VIS deque {
+  static_assert(!is_const<_Tp>::value, "'std::deque' can only hold non-const types");
+  static_assert(!is_volatile<_Tp>::value, "'std::deque' can only hold non-volatile types");
+  static_assert(!is_reference<_Tp>::value || !is_function<typename remove_reference<_Tp>::type>::value, "'std::deque' can only hold object types; function references are not objects (consider using a function pointer)");
+  static_assert(!is_reference<_Tp>::value, "'std::deque' can only hold object types; references are not objects");
+  static_assert(!is_function<_Tp>::value, "'std::deque' can only hold object types; functions are not objects (consider using a function pointer)");
+  static_assert(!is_void<_Tp>::value, "'std::deque' can only hold object types; 'void' is not an object");
 public:
   // types:
 
diff --git a/libcxx/include/forward_list b/libcxx/include/forward_list
index b8e3d05588f96e..2b6ad3bc100f90 100644
--- a/libcxx/include/forward_list
+++ b/libcxx/include/forward_list
@@ -220,11 +220,16 @@ template <class T, class Allocator, class Predicate>
 #include <__type_traits/conditional.h>
 #include <__type_traits/is_allocator.h>
 #include <__type_traits/is_const.h>
+#include <__type_traits/is_function.h>
 #include <__type_traits/is_nothrow_assignable.h>
 #include <__type_traits/is_nothrow_constructible.h>
 #include <__type_traits/is_pointer.h>
+#include <__type_traits/is_reference.h>
 #include <__type_traits/is_same.h>
 #include <__type_traits/is_swappable.h>
+#include <__type_traits/is_void.h>
+#include <__type_traits/is_volatile.h>
+#include <__type_traits/remove_reference.h>
 #include <__type_traits/type_identity.h>
 #include <__utility/forward.h>
 #include <__utility/move.h>
@@ -635,8 +640,136 @@ void __forward_list_base<_Tp, _Alloc>::clear() _NOEXCEPT {
   __before_begin()->__next_ = nullptr;
 }
 
+// begin-diagnostic-helpers
+// std::forward_list can only have non-cv-qualified object types as its value type, which we diagnose.
+// In order to short-cirucit redundant (and cryptic) diagnostics, std::forward_list must be the one
+// to fire the static_assert. Since std::forward_list inherits from __forward_list_base, we also need
+// to create specialisations for the rejected types, so that everything appears "good" to std::forward_list
+// (otherwise we'll get lots of unhelpful diagnostics that suppress the one std::forward_list offers).
+template <class _Tp, class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __forward_list_base<_Tp const, _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+
+template <class _Tp, class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __forward_list_base<_Tp volatile, _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+
+template <class _Tp, class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __forward_list_base<_Tp&, _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+
+template <class _Tp, class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __forward_list_base<_Tp&&, _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+
+template <class _Tp, class... _Args, class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __forward_list_base<_Tp(_Args...), _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+
+template <class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __forward_list_base<void, _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+// end-diagnostic-helpers
+
 template <class _Tp, class _Alloc /*= allocator<_Tp>*/>
 class _LIBCPP_TEMPLATE_VIS forward_list : private __forward_list_base<_Tp, _Alloc> {
+  static_assert(!is_const<_Tp>::value, "'std::forward_list' can only hold non-const types");
+  static_assert(!is_volatile<_Tp>::value, "'std::forward_list' can only hold non-volatile types");
+  static_assert(!is_reference<_Tp>::value || !is_function<typename remove_reference<_Tp>::type>::value, "'std::forward_list' can only hold object types; function references are not objects (consider using a function pointer)");
+  static_assert(!is_reference<_Tp>::value, "'std::forward_list' can only hold object types; references are not objects");
+  static_assert(!is_function<_Tp>::value, "'std::forward_list' can only hold object types; functions are not objects (consider using a function pointer)");
+  static_assert(!is_void<_Tp>::value, "'std::forward_list' can only hold object types; 'void' is not an object");
+
   typedef __forward_list_base<_Tp, _Alloc> base;
   typedef typename base::__node_allocator __node_allocator;
   typedef typename base::__node_type __node_type;
diff --git a/libcxx/include/list b/libcxx/include/list
index 929c84de7be449..8da28ff1873d89 100644
--- a/libcxx/include/list
+++ b/libcxx/include/list
@@ -226,10 +226,16 @@ template <class T, class Allocator, class Predicate>
 #include <__ranges/from_range.h>
 #include <__type_traits/conditional.h>
 #include <__type_traits/is_allocator.h>
+#include <__type_traits/is_const.h>
+#include <__type_traits/is_function.h>
 #include <__type_traits/is_nothrow_assignable.h>
 #include <__type_traits/is_nothrow_constructible.h>
 #include <__type_traits/is_pointer.h>
+#include <__type_traits/is_reference.h>
 #include <__type_traits/is_same.h>
+#include <__type_traits/is_void.h>
+#include <__type_traits/is_volatile.h>
+#include <__type_traits/remove_reference.h>
 #include <__type_traits/type_identity.h>
 #include <__utility/forward.h>
 #include <__utility/move.h>
@@ -659,8 +665,136 @@ void __list_imp<_Tp, _Alloc>::swap(__list_imp& __c)
     __c.__end_.__prev_->__next_ = __c.__end_.__next_->__prev_ = __c.__end_as_link();
 }
 
+// begin-diagnostic-helpers
+// std::list can only have non-cv-qualified object types as its value type, which we diagnose. In order
+// to short-cirucit redundant (and cryptic) diagnostics, std::list must be the one to fire the static_assert.
+// Since std::list inherits from __list_imp, we also need to create specialisations for the rejected types,
+// so that everything appears "good" to std::list (otherwise we'll get lots of unhelpful diagnostics that
+// suppress the one std::list offers).
+template <class _Tp, class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __list_imp<_Tp const, _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+
+template <class _Tp, class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __list_imp<_Tp volatile, _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+
+template <class _Tp, class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __list_imp<_Tp&, _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+
+template <class _Tp, class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __list_imp<_Tp&&, _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+
+template <class _Tp, class... _Args, class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __list_imp<_Tp(_Args...), _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+
+template <class _Alloc>
+class _LIBCPP_TEMPLATE_VIS __list_imp<void, _Alloc> {
+public:
+  using __node_allocator = void*;
+  using __node_alloc_traits = void*;
+  using __node_pointer = void*;
+  using __node_type = void*;
+  using __node_base = void*;
+  using __node_base_pointer = void*;
+  using __link_pointer = void*;
+
+  using pointer = int*;
+  using const_pointer = int const*;
+  using size_type = unsigned int;
+  using difference_type = int;
+  using iterator = int*;
+  using const_iterator = int const*;
+};
+// end-diagnostic-helpers
+
 template <class _Tp, class _Alloc /*= allocator<_Tp>*/>
 class _LIBCPP_TEMPLATE_VIS list : private __list_imp<_Tp, _Alloc> {
+  static_assert(!is_const<_Tp>::value, "'std::list' can only hold non-const types");
+  static_assert(!is_volatile<_Tp>::value, "'std::list' can only hold non-volatile types");
+  static_assert(!is_reference<_Tp>::value || !is_function<typename remove_reference<_Tp>::type>::value, "'std::list' can only hold object types; function references are not objects (consider using a function pointer)");
+  static_assert(!is_reference<_Tp>::value, "'std::list' can only hold object types; references are not objects");
+  static_assert(!is_function<_Tp>::value, "'std::list' can only hold object types; functions are not objects (consider using a function pointer)");
+  static_assert(!is_void<_Tp>::value, "'std::list' can only hold object types; 'void' is not an object");
+
   typedef __list_imp<_Tp, _Alloc> base;
   typedef typename base::__node_type __node_type;
   typedef typename base::__node_allocator __node_allocator;
diff --git a/libcxx/include/map b/libcxx/include/map
index 02bd17ccb4e8cb..3a801c0162a3f9 100644
--- a/libcxx/include/map
+++ b/libcxx/include/map
@@ -592,6 +592,9 @@ erase_if(multimap<Key, T, Compare, Allocator>& c, Predicate pred);  // C++20
 #include <__ranges/from_range.h>
 #include <__tree>
 #include <__type_traits/is_allocator.h>
+#include <__type_traits/is_function.h>
+#include <__type_traits/is_lvalue_reference.h>
+#include <__type_traits/is_void.h>
 #include <__utility/forward.h>
 #include <__utility/piecewise_construct.h>
 #include <__utility/swap.h>
@@ -962,6 +965,13 @@ public:
 
 template <class _Key, class _Tp, class _Compare = less<_Key>, class _Allocator = allocator<pair<const _Key, _Tp> > >
 class _LIBCPP_TEMPLATE_VIS map {
+  static_assert(!is_lvalue_reference<_Key>::value || !is_function<typename remove_reference<_Key>::type>::value, "'std::map::key_type' can only hold object types; function references are not objects (consider using a function pointer)");
+  static_assert(!is_lvalue_reference<_Key>::value, "'std::map::key_type' can only hold object types; references are not objects");
+  static_assert(!is_function<_Key>::value, "'std::map::key_type' can only hold object types; functions are not objects (consider using a function pointer)");
+  static_assert(!is_void<_Key>::value, "'std::map::key_type' can only hold object types; 'void' is not an object");
+
+  static_assert(!is_function<_Tp>::value, "'std::map::mapped_type' can only hold object or reference types; functions are neither (consider using a function pointer or reference)");
+  static_assert(!is_void<_Tp>::value, "'std::map::mapped_type' can only hold object types; 'void' is not an object");
 public:
   // types:
   typedef _Key key_type;
@@ -1639,6 +1649,13 @@ erase_if(map<_Key, _Tp, _Compare, _Allocator>& __c, _Predicate __pred) {
 
 template <class _Key, class _Tp, class _Compare = less<_Key>, class _Allocator = allocator<pair<const _Key, _Tp> > >
 class _LIBCPP_TEMPLATE_VIS multimap {
+  static_assert(!is_lvalue_reference<_Key>::value || !is_function<typename remove_reference<_Key>::type>::value, "'std::multimap::key_type' can only hold object types; function references are not objects (consider using a function pointer)");
+  static_assert(!is_lvalue_reference<_Key>::value, "'std::multimap::key_type' can only hold object types; references are not objects");
+  static_assert(!is_function<_Key>::value, "'std::multimap::key_type' can only hold object types; functions are not objects (consider using a function pointer)");
+  static_assert(!is_void<_Key>::value, "'std::multimap::key_type' can only hold object types; 'void' is not an object");
+
+  static_assert(!is_function<_Tp>::value, "'std::multimap::mapped_type' can only hold object or reference types; functions are neither (consider using a function pointer or reference)");
+  static_assert(!is_void<_Tp>::value, "'std::multimap::mapped_type' can only hold object types; 'void' is not an object");
 public:
   // types:
   typedef _Key key_type;
diff --git a/libcxx/include/set b/libcxx/include/set
index 94533583798699..c5c1f664283648 100644
--- a/libcxx/include/set
+++ b/libcxx/include/set
@@ -531,6 +531,12 @@ erase_if(multi...
[truncated]

@cjdb
Copy link
Contributor Author

cjdb commented Aug 27, 2024

The motivation for this patch comes from routinely getting downstream user bugs about the helpfulness of the current diagnostics.

Copy link

github-actions bot commented Aug 27, 2024

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

`std::vector<int&>` generates nearly 170 lines of diagnostic, most of
which are redundant, and it's only helpful if you are already familiar
with the rule that containers can't hold reference types. This commit
reduces it to only a handful of lines, all of which are dedicated to
clearly communicating the problem. It also applies this to all the
other containers, and for all non-cv-qualified and non-object types.

These static_asserts are placed at the very top of each container
because they short-circuit further instantiation errors, thereby
leading a smaller set of diagnostics for the programmer. Placing them
in `std::allocator` (which is the common denominator for all containers)
doesn't do the short circuiting, and we thus end up with several
unhelpful diagnostics after the helpful one.

This commit only cleans up things that are already diagnosed as
compile-time errors. In particular, `map<int&&, int>` should be
ill-formed, per [associative.reqmts.general]/p8. libc++ and libstdc++
currently permit that, but not `map<int&, int>`. As such, this
commit only adds a static assert for `map<K&, V>`, and defers diagnosing
valid productions that should be ill-formed to a future commit.
@cjdb cjdb force-pushed the cleaner-libcxx-diagnostics branch from 59e11c6 to 5529499 Compare August 27, 2024 21:47
Comment on lines 82 to 88
static_assert(!is_const<_Tp>::value, "'std::allocator' can only allocate non-const object types");
static_assert(!is_volatile<_Tp>::value, "'std::allocator' can only allocate non-volatile object types");
static_assert(!is_reference<_Tp>::value || !is_function<typename remove_reference<_Tp>::type>::value,
"'std::allocator' can only allocate object types; function references are not objects (consider using "
"a function pointer)");
static_assert(!is_reference<_Tp>::value,
"'std::allocator' can only allocate object types; references are not objects");
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we just check is_same<remove_cvref_t<_Tp>, _Tp> instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That erases the tailored diagnostics; noting references are not objects, etc., are hints I that believe significantly improves UX.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Folks internally suggested that I change the text to the following, as they feel it'll be more helpful than what I'm currently proposing.

  • std::allocator<T const>: 'std::allocator' cannot allocate const objects (return to status quo)
  • std::allocator<T volatile>: 'std::allocator' cannot allocate volatile objects (return to status quo)
  • std::allocator<T&>: 'std::allocator' cannot allocate references
  • std::allocator<T()>: 'std::allocator' cannot allocate functions

We do lose the function reference suggestion, but I'm okay with doing that for something that's more helpful.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not simply "cannot allocate cv or reference qualified objects"? I'm not sure how much of an improvement it is to specify the qualifier explicitly, since it's already shown in the diagnostic.

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 kicked the internal discussion off by asking if the average C++ programmer would be able to understand 'std::vector' can only allocate non-cv-qualified object types, and the feedback I've received can be aggregated as roughly

  • most C++ programmers don't know what the term "cv-qualified" means
  • we should only mention relevant information, and mentioning "volatile" when volatile isn't used was repeatedly called out in particular
  • most C++ programmers probably aren't going to know that "object" has a very different meaning to other languages
  • most non-toolchain folks in the discussion seemed to prefer separate messages; toolchain folks were sort of ambivalent

What do you feel a single check will offer over independent checks?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is your concern with respect to the number of instantiations?

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, I'm concerned that we instantiate a bunch of types for the rare case they are actually required for diagnostic purposes. Every instantiation takes time and memory, and for a type that is used everywhere, like vector, we should keep unnecessary instantiations as low as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It sounds to me like you're concerned about build times. Have you run any preliminary analysis to determine that there is a noticeable impact?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is more a death by a thousand cuts situation than something significant I think. If we add this many checks everywhere people will definitely notice, but not if we just add it in a single place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very good point, I agree. I think I've got something that bridges the gap between what you suggested above, and what I was originally hoping to land.

cjdb added 10 commits August 29, 2024 00:45
I think the best way to enact `git clang-format` might be to use `git
clang-format <hash-before-your-commit-first>` instead of `git
clang-format HEAD~n`. This ensures that all commits in a PR are
formatted, instead of just the last n (which one might miscount, like I
probably did).
The trick is to run

```
$ git-clang-format --diff <base-hash> --extensions ,cpp,h | patch -p1
```
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.

I like this patch a lot more in its current form. I still have a few comments but the direction makes sense to me!

Comment on lines 82 to 85
static_assert(!is_const<_Tp>::value, "'std::allocator' cannot allocate const types");
static_assert(!is_volatile<_Tp>::value, "'std::allocator' cannot allocate volatile types");
static_assert(!is_reference<_Tp>::value, "'std::allocator' cannot allocate references");
static_assert(!is_function<_Tp>::value, "'std::allocator' cannot allocate functions");
Copy link
Member

Choose a reason for hiding this comment

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

Can we tie these checks back to the Standard wording that says so? It doesn't have to be in the static assert message, but at least in a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

PTAL

libcxx/include/deque Outdated Show resolved Hide resolved
@cjdb
Copy link
Contributor Author

cjdb commented Aug 29, 2024

I think I need to walk back my is_array change, since I think libc++ might be incorrectly rejecting vector<char[2]>. This is conditional on whether we're in C++20 or not.

A Clang pragma that lets us "hide" macro expansion from diagnostics so
that users don't need to read implementation details would be a good
thing to explore in the future.
// allocator that meets the Cpp17Allocator requirements (https://eel.is/c++draft/allocator.requirements).
// In particular, this means that containers should only accept non-cv-qualified object types, and
// types that are Cpp17Erasable.
#define _LIBCPP_CHECK_CONTAINER_VALUE_TYPE_REQUIREMENTS_BASE(_Container, _Tp) \
Copy link
Member

Choose a reason for hiding this comment

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

I don't see this macro being used outside of the definition of _LIBCPP_CHECK_CONTAINER_VALUE_TYPE_REQUIREMENTS_FULL -- is that an oversight or should this be inlined into _LIBCPP_CHECK_CONTAINER_VALUE_TYPE_REQUIREMENTS_FULL?

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 think I was experimenting to identify the bare minimum for all containers, and possibly forgot about that due to the (long) weekend.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah! It was an attempt to share the content with std::allocator, not to handle std::array and the map types (the latter of which will be converted after the clean-up).

libcxx/include/__type_traits/diagnostic_utilities.h Outdated Show resolved Hide resolved
libcxx/include/__memory/allocator.h Outdated Show resolved Hide resolved
libcxx/include/array Outdated Show resolved Hide resolved
libcxx/include/deque Outdated Show resolved Hide resolved
cjdb and others added 4 commits September 3, 2024 11:13
Co-authored-by: Louis Dionne <ldionne.2@gmail.com>
`std::llocator` apparently supports unbounded arrays prior to C++20, so
we need to change the `is_array` check to only check for bounded arrays.
libcxx/include/__type_traits/diagnostic_utilities.h Outdated Show resolved Hide resolved

#define _LIBCPP_CHECK_CONTAINER_VALUE_TYPE_REQUIREMENTS(_Container, _Tp) \
static_assert( \
!__libcpp_is_unbounded_array<_Tp>::value, "'std::" _Container "' cannot hold C arrays of an unknown size"); \
Copy link
Member

Choose a reason for hiding this comment

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

Instead of passing vector as the _Container argument, I'd pass std::vector and simplify the message here. The message would become _Container " cannot hold C arrays of an unknown size".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I'd prefer to keep the single quotes so that it's consistent with how Clang outputs type information. For example, this is the diagnostic we get for this erroneous code:

namespace std {
  template<class T>
  struct S1;

  template<class T>
  struct S2;
}

std::S1<std::S2> y;
error: use of class template 'std::S2' requires template arguments

This helps us remain more consistent as a whole implementation:

error: static assertion failed: 'std::vector' cannot hold C arrays of an unknown size

WDYT?

Comment on lines +1510 to +1517
export std_private_type_traits_diagnostic_utilities
export std_private_type_traits_is_bounded_array
export std_private_type_traits_is_const
export std_private_type_traits_is_function
export std_private_type_traits_is_reference
export std_private_type_traits_is_unbounded_array
export std_private_type_traits_is_void
export std_private_type_traits_is_volatile
Copy link
Member

Choose a reason for hiding this comment

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

Did you run into CI issues if you didn't do that? I don't think these need to be exported from the module since they are only being used in the implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'm puzzled too. Our CI build script wasn't happy when building the modules. Might be worth trying on your end to see if I'm doing something silly?

Comment on lines 1032 to 1033
// TODO(#106635): replace with _LIBCPP_CHECK_CONTAINER_VALUE_TYPE_REQUIREMENTS
// Remember to remove relevant headers when this is completed.
Copy link
Member

Choose a reason for hiding this comment

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

I read the description of microsoft/STL#3660 and it looks like the Standard does allow these types to be used. If that's the case, then #106635 should be closed until we have a LWG issue or a paper that changes the Standard, and these TODOs should be removed since we won't be able to address them.

Note that I have no love for reference types in these locations, but I also want to avoid adding non-actionable TODOs.

Copy link
Contributor Author

@cjdb cjdb Sep 5, 2024

Choose a reason for hiding this comment

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

I think MS/STL is allowing these as an extension? I see that the PR mentions that the standard permits this, but my interpretation of the linked wording from #106635 already prohibits unordered_map<int, int&>. I've asked for input in microsoft/STL#3660.

Nonetheless, we weren't catching it before, and have been Hyrum's Law'd.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I get what's happening, thanks to cplusplus/draft#7249. I've parked #106635, and have replaced the TODOs with // not an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Per #106635 (comment), I've reopened the issue, and have reverted bb82c95.

Copy link
Member

Choose a reason for hiding this comment

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

So IIUC basically references in these locations are not permitted, and we should clean up the usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

References, const, volatile, etc. I'm especially leery of map<int const, int>, because it's in no way interface-distinguishable from map<int, int>, yet is its own instantiation.

I'm happy to take point on that over the next few releases, but we'll need to come up with a plan (and potentially coordinate with MS/STL and libstdc++).

Nikolas pointed out that checking all the traits in all cases will
probably grind builds to a halt as we improve our diagnostics, so this
commit makes a point of reducing the number of instantiations as much as
possible, and preserves the highly specific messages.
libcxx/include/__type_traits/diagnostic_utilities.h Outdated Show resolved Hide resolved
libcxx/include/__type_traits/diagnostic_utilities.h Outdated Show resolved Hide resolved
struct __bounded_arrays_allowed_only_after_cxx20 : integral_constant<bool, __libcpp_is_bounded_array<_Tp>::value> {};
#endif

#define _LIBCPP_CHECK_ALLOCATOR_VALUE_TYPE_REQUIREMENTS(_Template, _Verb) \
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't have to _Uglify macro parameters.


#define _LIBCPP_DEFINE__REQUIRES_CV_UNQUALIFIED_OBJECT_TYPE(_Template, _Verb) \
template <class _Tp> \
struct __requires_cv_unqualified_object_type<_Template, _Tp, false> \
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason why the patch now defines these __requires_cv_unqualified_object_type specializations for each container? Was that in response to the comment about too many instantiations?

This adds complexity but I don't see the benefit (yet), I'd rather keep this patch as simple as can be. After all the goal here is to issue diagnostics -- that's a fairly simple problem and I'd like to keep the solution accordingly simple.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a reason why the patch now defines these __requires_cv_unqualified_object_type specializations for each container? Was that in response to the comment about too many instantiations?

Yes. I ended up benchmarking Chrome's build times and while we could probably tolerate this patch in isolation, stacking similar changes will eventually tank build times.

This adds complexity but I don't see the benefit (yet), I'd rather keep this patch as simple as can be. After all the goal here is to issue diagnostics -- that's a fairly simple problem and I'd like to keep the solution accordingly simple.

Issuing simple diagnostics is simple. The goal of this patch is to issue diagnostics that are informative to the reader, using language that they're likely to understand; achieving that without adversely impacting build times is apparently more challenging to simplify.

Could you help me understand what it is about the complexity that raises concern, please?

Copy link
Member

Choose a reason for hiding this comment

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

Basically, I don't understand why the macros don't expand to a bunch of static_asserts directly (like it used to do), instead of going through an additional partial specialization of __requires_cv_unqualified_object_type & friends. That's a complex mechanism for doing basically static_assert(condition) and I don't understand what we're gaining from that complexity. In fact, I would expect this approach to have more impact on build times since we instantiate more stuff and we have a partial specialization.

Copy link
Contributor Author

@cjdb cjdb Sep 19, 2024

Choose a reason for hiding this comment

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

Ah, I get where you're coming from now, thank you. This is a metaprogramming hack to preserve performance for valid use.

  • In the case where decay_t<_Tp> matches _Tp: __requires_cv_unqualified_type should only need to instantiate two templates: decay_t<_Tp> and __requires_cv_unqualified_type<_Template, _Tp, true>.
  • In case where decay_t<_Tp> is different to _Tp: __requires_cv_unqualified_type then instantiates all the checks to see why that isn't the case. This is indeed slower, but we're now going for a failed build, and tooling tends to burn compile times in favour of helpful diagnostics.

This is in contrast to a raw macro, which performs all the checks, regardless of validity (which cakes on time as you use more unique container templates, to the point of being noticeable).

Neither __allocator_requirements, nor __container_requirements have the same effect, so I'm happy to revert those without further convincing.

Chromium is a good candidate for benchmarking the worst-case scenario, because it has well over 100k utterances of std::vector. Would providing benchmarks be helpful? (It'll take ~a day to get those as I discarded the results from last week, and I tend to run quite a few trials to reduce the impact of noise.)

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.

5 participants