Propagate T::index_type through partial_range

When partial_range is used on a container that defines index_type,
define that same index_type on the partial_range.  This allows other
callers, such as enumerate(), to maintain the correct index type.
This commit is contained in:
Kp 2021-09-04 12:17:14 +00:00
parent 16aa554c0e
commit b6bebcf6b8
3 changed files with 179 additions and 43 deletions

View file

@ -6,5 +6,5 @@
*/
#pragma once
template <typename I>
template <typename range_iterator, typename range_index_type = void>
class partial_range_t;

View file

@ -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 <std::size_t NF, std::size_t NE>
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 <std::size_t N>
/* 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<char, N> &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 <typename>
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<typename std::remove_reference<range_type>::type::index_type &>::type,
/* If `range_type::index_type` is not a suitable argument to
* range_type::operator[](), fail.
*/
typename = decltype(std::declval<range_type &>().operator[](std::declval<index_type>()))
>
index_type range_index_type(std::nullptr_t);
}
#if DXX_PARTIAL_RANGE_MINIMIZE_ERROR_TYPE
struct partial_range_error;
#endif
template <typename I>
template <typename range_iterator, typename range_index_type>
class partial_range_t
{
public:
static_assert(!std::is_reference<range_iterator>::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<iterator>{m_begin};
}
[[nodiscard]]
partial_range_t<std::reverse_iterator<iterator>> reversed() const
partial_range_t<std::reverse_iterator<iterator>, 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 <typename I>
struct partial_range_t<I>::partial_range_error
template <typename range_iterator, typename range_index_type>
struct partial_range_t<range_iterator, range_index_type>::partial_range_error
#endif
final : std::out_of_range
{
@ -191,6 +220,7 @@ namespace partial_range_detail
{
template <typename range_exception, std::size_t required_buffer_size>
__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 <typename range_exception, std::size_t required_buffer_size, typename P>
__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<iterator_type> unchecked_partial_range_advance(
inline partial_range_t<iterator_type, index_type> 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<iterator_type> unchecked_partial_range_advance(
/* Avoid iterator dereference if range is empty */
if (index_end)
{
partial_range_detail::check_range_object_size<typename partial_range_t<iterator_type>::partial_range_error, required_buffer_size>(file, line, estr, *range_begin, index_begin, index_end);
partial_range_detail::check_range_object_size<typename partial_range_t<iterator_type, index_type>::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<range_type &>())),
/* 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<iterator_type>())
>
[[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<index_begin_type>::value, "offset to partial_range must be unsigned");
static_assert(std::is_unsigned<index_end_type>::value, "length to partial_range must be unsigned");
static_assert(!std::is_void<reference>::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<range_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<!std::is_array<iterator_type>::value, decltype(*std::declval<iterator_type>())>::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<index_begin_type>::value, "offset to partial_range must be unsigned");
static_assert(std::is_unsigned<index_end_type>::value, "length to partial_range must be unsigned");
static_assert(!std::is_void<reference>::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<iterable>(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<T &>()))
typename iterator_type = decltype(std::begin(std::declval<range_type &>()))
>
[[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<typename partial_range_t<I>::partial_range_error, required_buffer_size>(file, line, estr, reinterpret_cast<uintptr_t>(std::addressof(t)), index_begin, index_end, std::size(t));
partial_range_detail::check_range_bounds<
typename partial_range_t<iterator_type, decltype(partial_range_detail::range_index_type<range_type>(nullptr))>::partial_range_error,
required_buffer_size
>(file, line, estr, reinterpret_cast<uintptr_t>(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<required_buffer_size, T, index_end_type, index_end_type>(file, line, estr, t, index_end_type{}, index_end);
return partial_range<required_buffer_size, range_type, index_end_type, index_end_type>(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<required_buffer_size, const T, index_begin_type, index_end_type>(file, line, estr, t, index_begin, index_end);
return partial_range<required_buffer_size, const range_type, index_begin_type, index_end_type>(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<required_buffer_size, const T, index_end_type>(file, line, estr, t, index_end);
return partial_range<required_buffer_size, const range_type, index_end_type>(file, line, estr, range, index_end);
}
template <typename T, typename I = decltype(std::begin(std::declval<T &>()))>
template <typename range_type>
[[nodiscard]]
inline partial_range_t<I> (make_range)(T &t)
inline partial_range_t<
decltype(std::begin(std::declval<range_type &>())),
decltype(partial_range_detail::range_index_type<range_type>(nullptr))
> (make_range)(range_type &t)
{
return t;
}
@ -431,18 +527,23 @@ inline partial_range_t<I> (make_range)(T &t)
*/
template <typename T,
typename index_begin_type,
typename index_end_type,
typename I = decltype(begin(std::declval<T &&>()))
typename index_end_type
>
partial_range_t<I> (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<decltype(begin(std::declval<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 T,
typename index_end_type,
typename I = decltype(begin(std::declval<T &&>()))
typename index_end_type
>
partial_range_t<I> (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<decltype(begin(std::declval<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<partial_range_detail::required_buffer_size<sizeof(__FILE__), sizeof(#T)>>(__FILE__, __LINE__, #T, T, ##__VA_ARGS__)
#endif
#define partial_range(T,...) partial_range<partial_range_detail::required_buffer_size<sizeof(__FILE__), sizeof(#T)>>(__FILE__, __LINE__, #T, T, ##__VA_ARGS__)

View file

@ -1,4 +1,5 @@
#include "partial_range.h"
#include <type_traits>
#include <vector>
#define BOOST_TEST_DYN_LINK
@ -8,6 +9,8 @@
#define DXX_TEST_IGNORE_RETURN(EXPR) ({ auto &&r = EXPR; static_cast<void>(r); })
#define OPTIMIZER_HIDE_VARIABLE(V) asm("" : "=rm" (V) : "0" (V) : "memory")
BOOST_TEST_SPECIALIZED_COLLECTION_COMPARE(std::vector<int>);
BOOST_AUTO_TEST_CASE(exception_past_end)
{
std::vector<int> vec;
@ -97,3 +100,35 @@ BOOST_AUTO_TEST_CASE(range_slice_reversed_begin_1_end_3)
std::vector<int> 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 <typename Expected, typename partial_range_type, typename index_type = typename partial_range_type::index_type>
struct assert_index_type : std::true_type
{
static_assert(std::is_same<Expected, index_type>::value);
};
static_assert(assert_index_type<void, decltype(partial_range(std::declval<std::vector<int>&>(), 0u, 1u))>::value);
template <typename T>
struct custom_index_type_only : std::array<int, 1>
{
using index_type = T;
};
template <typename T>
struct custom_index_type : std::array<int, 1>
{
using index_type = T;
void operator[](typename std::remove_reference<T>::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<void, decltype(partial_range(std::declval<custom_index_type_only<int *>&>(), 0u, 1u))>::value);
static_assert(assert_index_type<std::size_t, decltype(partial_range(std::declval<custom_index_type<std::size_t>&>(), 0u, 1u))>::value);
static_assert(assert_index_type<e1, decltype(partial_range(std::declval<custom_index_type<e1>&>(), 0u, 1u))>::value);