diff --git a/common/include/fwd-partial_range.h b/common/include/fwd-partial_range.h index bf2ecb9f7..b626e497e 100644 --- a/common/include/fwd-partial_range.h +++ b/common/include/fwd-partial_range.h @@ -6,5 +6,5 @@ */ #pragma once -template +template class partial_range_t; diff --git a/common/include/partial_range.h b/common/include/partial_range.h index c50c04232..0919e793c 100644 --- a/common/include/partial_range.h +++ b/common/include/partial_range.h @@ -48,13 +48,24 @@ namespace partial_range_detail { #define REPORT_FORMAT_STRING "%s:%u: %s %lu past %p end %lu \"%s\"" -/* Round reporting into large buckets. Code size is more - * important than stack space on a cold path. +/* Given the length of a filename (in `NF`) and the length of an + * expression to report (in `NE`), compute the required size of a buffer + * that can hold the result of applying REPORT_FORMAT_STRING. This + * overestimates the size since sizeof(REPORT_FORMAT_STRING) is used + * without accounting for the %-escapes being replaced with the expanded + * text, and all expansions are assumed to be of maximum length. + * + * Round reporting into large buckets. Code size is more important than + * stack space on a cold path. */ template constexpr std::size_t required_buffer_size = ((sizeof(REPORT_FORMAT_STRING) + sizeof("65535") + (sizeof("18446744073709551615") * 2) + sizeof("0x0000000000000000") + (NF + NE + sizeof("begin"))) | 0xff) + 1; template + /* This is only used on a path that will throw an exception, so mark + * it as cold. In a program with no bugs, which is given no + * ill-formed data on input, these exceptions never happen. + */ __attribute_cold void prepare_error_string(std::array &buf, unsigned long d, const char *estr, const char *file, unsigned line, const char *desc, unsigned long expr, const uintptr_t t) { @@ -81,18 +92,36 @@ inline auto adl_end(T &t) return end(t); } +template +void range_index_type(...); + +template < + typename range_type, + /* If `range_type::index_type` is not defined, fail. + * If `range_type::index_type` is void, fail. + */ + typename index_type = typename std::remove_reference::type::index_type &>::type, + /* If `range_type::index_type` is not a suitable argument to + * range_type::operator[](), fail. + */ + typename = decltype(std::declval().operator[](std::declval())) + > +index_type range_index_type(std::nullptr_t); + } #if DXX_PARTIAL_RANGE_MINIMIZE_ERROR_TYPE struct partial_range_error; #endif -template +template class partial_range_t { public: + static_assert(!std::is_reference::value); using range_owns_iterated_storage = std::false_type; - typedef I iterator; + using iterator = range_iterator; + using index_type = range_index_type; /* When using the unminimized type, forward declare a structure. * * When using the minimized type, add a typedef here so that later @@ -161,7 +190,7 @@ public: return std::reverse_iterator{m_begin}; } [[nodiscard]] - partial_range_t> reversed() const + partial_range_t, index_type> reversed() const { return {rbegin(), rend()}; } @@ -170,8 +199,8 @@ public: #if DXX_PARTIAL_RANGE_MINIMIZE_ERROR_TYPE struct partial_range_error #else -template -struct partial_range_t::partial_range_error +template +struct partial_range_t::partial_range_error #endif final : std::out_of_range { @@ -191,6 +220,7 @@ namespace partial_range_detail { template +__attribute_always_inline() inline void check_range_bounds(const char *file, unsigned line, const char *estr, const uintptr_t t, const std::size_t index_begin, const std::size_t index_end, const std::size_t d) { #ifdef DXX_CONSTANT_TRUE @@ -233,6 +263,7 @@ inline void check_range_bounds(const char *file, unsigned line, const char *estr #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE template +__attribute_always_inline() inline void check_range_object_size(const char *file, unsigned line, const char *estr, P &ref, const std::size_t index_begin, const std::size_t index_end) { const auto ptr = std::addressof(ref); @@ -268,10 +299,11 @@ template < #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE std::size_t required_buffer_size, #endif + typename index_type, typename iterator_type > [[nodiscard]] -inline partial_range_t unchecked_partial_range_advance( +inline partial_range_t unchecked_partial_range_advance( #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE const char *const file, const unsigned line, const char *const estr, #endif @@ -290,7 +322,7 @@ inline partial_range_t unchecked_partial_range_advance( /* Avoid iterator dereference if range is empty */ if (index_end) { - partial_range_detail::check_range_object_size::partial_range_error, required_buffer_size>(file, line, estr, *range_begin, index_begin, index_end); + partial_range_detail::check_range_object_size::partial_range_error, required_buffer_size>(file, line, estr, *range_begin, index_begin, index_end); } #endif auto range_end = range_begin; @@ -310,29 +342,37 @@ template < #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE std::size_t required_buffer_size, #endif - typename I, + typename range_type, typename index_begin_type, - typename index_end_type + typename index_end_type, + typename iterator_type = decltype(std::begin(std::declval())), + /* This is in the template signature so that an `iterator_type` + * which does not provide `operator*()` will trigger an error + * and remove this overload from the resolution set. + */ + typename reference = decltype(*std::declval()) > [[nodiscard]] +__attribute_always_inline() inline auto (unchecked_partial_range)( #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE const char *const file, const unsigned line, const char *const estr, #endif - const I range_begin, const index_begin_type &index_begin, const index_end_type &index_end) + range_type &range, const index_begin_type &index_begin, const index_end_type &index_end) { /* Require unsigned length */ static_assert(std::is_unsigned::value, "offset to partial_range must be unsigned"); static_assert(std::is_unsigned::value, "length to partial_range must be unsigned"); + static_assert(!std::is_void::value, "dereference of iterator must not be void"); return unchecked_partial_range_advance< #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE required_buffer_size, #endif - I>( + decltype(partial_range_detail::range_index_type(nullptr)), iterator_type>( #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE file, line, estr, #endif - range_begin, index_begin, index_end + std::begin(range), index_begin, index_end ); } @@ -340,7 +380,57 @@ template < #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE std::size_t required_buffer_size, #endif - typename I, + typename iterator_type, + typename index_begin_type, + typename index_end_type, + /* C arrays (`int a[5];`) can match both the overload that calls + * `std::begin(range)` and the overload that calls + * `operator*(range)`, leading to an ambiguity. Some supporting + * libraries define C arrays, which callers use partial_range on, so + * the array use cannot be converted to std::array. Use + * std::enable_if to disable this overload in the case of a C array. + * + * C++ std::array does not permit `operator*(range)`, and so does + * not match this overload, regardless of whether std::enable_if is + * used. + */ + typename reference = typename std::enable_if::value, decltype(*std::declval())>::type + > +[[nodiscard]] +inline auto (unchecked_partial_range)( +#ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE + const char *const file, const unsigned line, const char *const estr, +#endif + iterator_type iterator, const index_begin_type &index_begin, const index_end_type &index_end) +{ + /* Require unsigned length */ + static_assert(std::is_unsigned::value, "offset to partial_range must be unsigned"); + static_assert(std::is_unsigned::value, "length to partial_range must be unsigned"); + static_assert(!std::is_void::value, "dereference of iterator must not be void"); + /* Do not try to guess an index_type when supplied an iterator. Use + * `void` to state that no index_type is available. Callers which + * need a defined `index_type` should use the range-based + * unchecked_partial_range() instead, which can extract an + * index_type from the range_type. + */ + return unchecked_partial_range_advance< +#ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE + required_buffer_size, +#endif + void, iterator_type>( +#ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE + file, line, estr, +#endif + std::move(iterator), index_begin, index_end + ); +} + +/* Take either a range or an iterator, and forward it to an appropriate + * function, adding an index_begin={} along the way. + */ +template < + std::size_t required_buffer_size, + typename iterable, typename index_end_type > [[nodiscard]] @@ -348,80 +438,86 @@ inline auto (unchecked_partial_range)( #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE const char *const file, const unsigned line, const char *const estr, #endif - const I range_begin, const index_end_type &index_end) + iterable &&it, const index_end_type &index_end) { return unchecked_partial_range< #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE - required_buffer_size, + required_buffer_size #endif - I, index_end_type, index_end_type>( + >( #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE file, line, estr, #endif - range_begin, index_end_type{}, index_end + std::forward(it), index_end_type{}, index_end ); } template < std::size_t required_buffer_size, - typename T, + typename range_type, typename index_begin_type, typename index_end_type, - typename I = decltype(std::begin(std::declval())) + typename iterator_type = decltype(std::begin(std::declval())) > [[nodiscard]] -inline auto (partial_range)(const char *const file, const unsigned line, const char *const estr, T &t, const index_begin_type &index_begin, const index_end_type &index_end) +inline auto (partial_range)(const char *const file, const unsigned line, const char *const estr, range_type &range, const index_begin_type &index_begin, const index_end_type &index_end) { - partial_range_detail::check_range_bounds::partial_range_error, required_buffer_size>(file, line, estr, reinterpret_cast(std::addressof(t)), index_begin, index_end, std::size(t)); + partial_range_detail::check_range_bounds< + typename partial_range_t(nullptr))>::partial_range_error, + required_buffer_size + >(file, line, estr, reinterpret_cast(std::addressof(range)), index_begin, index_end, std::size(range)); return unchecked_partial_range< #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE required_buffer_size, #endif - I, index_begin_type, index_end_type>( + range_type, index_begin_type, index_end_type>( #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE file, line, estr, #endif - std::begin(t), index_begin, index_end + range, index_begin, index_end ); } template < std::size_t required_buffer_size, - typename T, + typename range_type, typename index_end_type > [[nodiscard]] -inline auto (partial_range)(const char *const file, const unsigned line, const char *const estr, T &t, const index_end_type &index_end) +inline auto (partial_range)(const char *const file, const unsigned line, const char *const estr, range_type &range, const index_end_type &index_end) { - return partial_range(file, line, estr, t, index_end_type{}, index_end); + return partial_range(file, line, estr, range, index_end_type{}, index_end); } template < std::size_t required_buffer_size, - typename T, + typename range_type, typename index_begin_type, typename index_end_type > [[nodiscard]] -inline auto (partial_const_range)(const char *const file, const unsigned line, const char *const estr, const T &t, const index_begin_type &index_begin, const index_end_type &index_end) +inline auto (partial_const_range)(const char *const file, const unsigned line, const char *const estr, const range_type &range, const index_begin_type &index_begin, const index_end_type &index_end) { - return partial_range(file, line, estr, t, index_begin, index_end); + return partial_range(file, line, estr, range, index_begin, index_end); } template < std::size_t required_buffer_size, - typename T, + typename range_type, typename index_end_type > [[nodiscard]] -inline auto (partial_const_range)(const char *const file, const unsigned line, const char *const estr, const T &t, const index_end_type &index_end) +inline auto (partial_const_range)(const char *const file, const unsigned line, const char *const estr, const range_type &range, const index_end_type &index_end) { - return partial_range(file, line, estr, t, index_end); + return partial_range(file, line, estr, range, index_end); } -template ()))> +template [[nodiscard]] -inline partial_range_t (make_range)(T &t) +inline partial_range_t< + decltype(std::begin(std::declval())), + decltype(partial_range_detail::range_index_type(nullptr)) +> (make_range)(range_type &t) { return t; } @@ -431,18 +527,23 @@ inline partial_range_t (make_range)(T &t) */ template ())) + typename index_end_type > -partial_range_t (partial_const_range)(const char *file, const unsigned line, const char *estr, const T &&t, const index_begin_type &index_begin, const index_end_type &index_end) = delete; +partial_range_t()))> (partial_const_range)(const char *file, const unsigned line, const char *estr, const T &&t, const index_begin_type &index_begin, const index_end_type &index_end) = delete; template ())) + typename index_end_type > -partial_range_t (partial_const_range)(const char *file, const unsigned line, const char *estr, const T &&t, const index_end_type &index_end) = delete; +partial_range_t()))> (partial_const_range)(const char *file, const unsigned line, const char *estr, const T &&t, const index_end_type &index_end) = delete; #ifdef DXX_HAVE_BUILTIN_OBJECT_SIZE +/* This macro is conditionally defined, because in the #else case, a + * bare invocation supplies all required parameters. In the #else case: + * - `required_buffer_size` is not a template parameter + * - all other template parameters can be deduced + * - the file, line, and string form of the expression are not function + * parameters. + */ #define unchecked_partial_range(T,...) unchecked_partial_range>(__FILE__, __LINE__, #T, T, ##__VA_ARGS__) #endif #define partial_range(T,...) partial_range>(__FILE__, __LINE__, #T, T, ##__VA_ARGS__) diff --git a/common/unittest/partial_range.cpp b/common/unittest/partial_range.cpp index 33eecb301..93505dda5 100644 --- a/common/unittest/partial_range.cpp +++ b/common/unittest/partial_range.cpp @@ -1,4 +1,5 @@ #include "partial_range.h" +#include #include #define BOOST_TEST_DYN_LINK @@ -8,6 +9,8 @@ #define DXX_TEST_IGNORE_RETURN(EXPR) ({ auto &&r = EXPR; static_cast(r); }) #define OPTIMIZER_HIDE_VARIABLE(V) asm("" : "=rm" (V) : "0" (V) : "memory") +BOOST_TEST_SPECIALIZED_COLLECTION_COMPARE(std::vector); + BOOST_AUTO_TEST_CASE(exception_past_end) { std::vector vec; @@ -97,3 +100,35 @@ BOOST_AUTO_TEST_CASE(range_slice_reversed_begin_1_end_3) std::vector expected{3, 2}; BOOST_TEST(out == expected); } + +/* Type system tests can be done at compile-time. Applying them as + * static_assert can produce a better error message than letting it fail + * at runtime. + */ +template +struct assert_index_type : std::true_type +{ + static_assert(std::is_same::value); +}; + +static_assert(assert_index_type&>(), 0u, 1u))>::value); +template +struct custom_index_type_only : std::array +{ + using index_type = T; +}; + +template +struct custom_index_type : std::array +{ + using index_type = T; + void operator[](typename std::remove_reference::type); +}; +enum class e1 : unsigned char; + +/* The type is `void` because resolving index_type fails since `int *` + * is not a valid argument type to operator[]. + */ +static_assert(assert_index_type&>(), 0u, 1u))>::value); +static_assert(assert_index_type&>(), 0u, 1u))>::value); +static_assert(assert_index_type&>(), 0u, 1u))>::value);