views::maybe
Document #: | P1255R6 |
Date: | 2020-04-05 |
Project: | Programming Language C++ |
Audience: |
LEWG |
Reply-to: |
Steve Downey <sdowney2@bloomberg.net, sdowney@gmail.com> |
Abstract: This paper proposes views::maybe
a range adaptor that produces a view with cardinality 0 or 1 which adapts nullable types such as std::optional
and pointer to object types.
<ranges>
headernoexcept
nessRemove Readable as part of the specification, use the useful requirements from Readable
Refer to views::all Behavior of capture vs refer is similar to how views::all works over the expression it is given
Use wording ‘range adaptor object’ Match current working paper language
Removed views::maybe_has_value and views::maybe_value, instead requiring that the nullable type be dereferenceable and contextually convertible to bool.
Nullable
, for expositionConcept Nullable, which is Readable and contextually convertible to bool
Hold a copy when constructing a view over a nullable rvalue.
Introduced two exposition types, one safely holding a copy, the other referring to the nullable
In writing range transformation it is useful to be able to lift a nullable value into a view that is either empty or contains the value held by the nullable. The adapter views::single
fills a similar purpose for non-nullable values, lifting a single value into a view, and views::empty
provides a range of no values of a given type. A views::maybe
adaptor also allows nullable values to be treated as ranges when it is otherwise undesirable to make them containers, for example std::optional
.
std::vector<std::optional<int>> v{
std::optional<int>{42},
std::optional<int>{},
std::optional<int>{6 * 9}};
auto r = views::join(views::transform(v, views::maybe));
for (auto i : r) {
std::cout << i; // prints 42 and 54
}
In addition to range transformation pipelines, views::maybe
can be used in range based for loops, allowing the nullable value to not be dereferenced within the body. This is of small value in small examples in contrast to testing the nullable in an if statement, but with longer bodies the dereference is often far away from the test. Often the first line in the body of the if
is naming the dereferenced nullable, and lifting the dereference into the for loop eliminates some boilerplate code, the same way that range based for loops do.
{
auto&& opt = possible_value();
if (opt) {
// a few dozen lines ...
use(*opt); // is *opt OK ?
}
}
for (auto&& opt : views::maybe(possible_value())) {
// a few dozen lines ...
use(opt); // opt is OK
}
The view can be on a std::reference_wrapper
, allowing the underlying nullable to be modified:
std::optional o{7};
for (auto&& i : views::maybe(std::ref(o))) {
i = 9;
std::cout << "i=" << i << " prints 9\n";
}
std::cout << "o=" << *o << " prints 9\n";
Of course, if the nullable is empty, there is nothing in the view to modify.
auto oe = std::optional<int>{};
for (int i : views::maybe(std::ref(oe)))
std::cout << "i=" << i << '\n'; // does not print
Converting an optional type into a view can make APIs that return optional types, such a lookup operations, easier to work with in range pipelines.
std::unordered_set<int> set{1, 3, 7, 9};
auto flt = [=](int i) -> std::optional<int> {
if (set.contains(i))
return i;
else
return {};
};
for (auto i : ranges::iota_view{1, 10} | ranges::views::transform(flt)) {
for (auto j : views::maybe(i)) {
for (auto k : ranges::iota_view(0, j))
std::cout << '\a';
std::cout << '\n';
}
}
// Produce 1 ring, 3 rings, 7 rings, and 9 rings
Add a range adaptor object views::maybe
, returning a view over a nullable object, capturing by value temporary nullables. A nullable
object is one that is both contextually convertible to bool and for which the type produced by dereferencing is an equality preserving object. Non void pointers, std::optional
, and the proposed expected
[P0323R9] types all model nullable
. Function pointers do not, as functions are not objects. Iterators do not generally model nullable
, as they are not required to be contextually convertible to bool.
The basis of the design is to hybridize views::single
and views::empty
. If the underlying object claims to hold a value, as determined by checking if the object when converted to bool is true, begin
and end
of the view are equivalent to the address of the held value within the underlying object and one past the underlying object. If the underlying object does not have a value, begin
and end
return nullptr
.
A publically available implementation at https://github.com/steve-downey/view_maybe based on the Ranges implementation in cmcstl2 at https://github.com/CaseyCarter/cmcstl2 . There are no particular implementation difficulties or tricks. The declarations are essentially what is quoted in the Wording section and the implementations are described as Effects.
Compiler Explorer Link to Before/After Examples
Call LEWG’s attention to the use of ptrdiff_t
as the return type of size
(which is consistent with single_view
). The author has a weak preference for a signed type here, but a strong preference for consistency with other Range types.single_view
now uses size_t.
Call LEWG’s attention to removing the conditional noexcept constructors, consistent with single_view
.
Modify 24.2 Header
// [range.maybe], maybe view
template<copy_constructible T>
requires see below
class maybe_view;
namespace views { inline constexpr unspecified maybe = unspecified; }
1 maybe_view
produces a view
over a nullable
that is either empty if the nullable
is empty, or provides access to the contents of the nullable
object.
2 The name views::maybe
denotes a customization point object ([customization.point.object]). For some subexpression E
, the expression views::maybe(E)
is expression-equivalent to:
maybe_view{E}
, the view
specified below, if the expression is well formed, where decay-copy(E)
is moved into the maybe_view
views::maybe(E)
is ill-formed.[Note: Whenever views::maybe(E)
is a valid expression, it is a prvalue whose type models view
. — end note ]
3 [ Example:
— end example ]
nullable
1 Types that:
bool
iter_reference_t
of the type and the iter_reference_t
of the const type, will :
is_lvalue_reference
is_object
when the reference is removedconvertible_to
reference_wrapper
around a type that satifies nullable
model the exposition only nullable
concept
2 Given a value i
of type I
, I
models nullable
only if the expression *i
is equality-preserving. [ Note: The expression *i
is required to be valid via the exposition-only nullable
concept). — end note ]
3 For convienence, the exposition-only is-reference-wrapper-v
is used below.
// For Exposition
template <typename T>
struct is_reference_wrapper : false_type {};
template <typename T>
struct is_reference_wrapper<reference_wrapper<T>> : true_type {};
template <typename T>
inline constexpr bool is_reference_wrapper_v
= is_reference_wrapper<T>::value;
// For Exposition
template <class Ref, class ConstRef>
concept readable_references =
is_lvalue_reference_v<Ref> &&
is_object_v<remove_reference_t<Ref>> &&
is_lvalue_reference_v<ConstRef> &&
is_object_v<remove_reference_t<ConstRef>> &&
convertible_to<add_pointer_t<ConstRef>,
const remove_reference_t<Ref>*>;
template <class T>
concept nullable =
is_object_v<T> &&
requires(T& t, const T& ct) {
bool(ct); // Contextually bool
*t; // T& is deferenceable
*ct; // const T& is deferenceable
}
&& readable_references<iter_reference_t<T>, // Ref
iter_reference_t<const T>>; // ConstRef
template <class T>
concept wrapped_nullable =
is-reference-wrapper-v<T>
&& nullable<typename T::type>;
namespace std::ranges {
template <copy_constructible Maybe>
requires (nullable<Maybe> || wrapped-nullable<Maybe>)
class maybe_view : public view_interface<maybe_view<Maybe>> {
private:
using T = /* see below */
semiregular-box<Maybe> value_; // exposition only (see 24.7.2 [range.semi.wrap])
public:
constexpr maybe_view() = default;
constexpr explicit maybe_view(Maybe const& maybe);
constexpr explicit maybe_view(Maybe&& maybe);
template<class... Args>
requires constructible_from<Maybe, Args...>
constexpr maybe_view(in_place_t, Args&&... args);
constexpr T* begin() noexcept;
constexpr const T* begin() const noexcept;
constexpr T* end() noexcept;
constexpr const T* end() const noexcept;
constexpr size_t size() const noexcept;
constexpr T* data() noexcept;
constexpr const T* data() const noexcept;
};
}
// For Exposition
using T = remove_reference_t<iter_reference_t<typename unwrap_reference_t<Maybe>>>;
1 Effects: Initializes value_ with maybe.
2 Effects: Initializes value_ with move(maybe)
.
3 Effects: Initializes value_ as if by value_{in_place, forward<Args>(args)...}
.
4 Effects: Equivalent to: return data();
.
5 Effects: Equivalent to: return data() + size();
.
6 Effects: Equivalent to:
if constexpr (is-reference-wrapper-v<Maybe>) {
return bool(value_.get().get());
} else {
return bool(value_.get());
}
🔗
7 Effects: Equivalent to:
Maybe& m = value_.get();
if constexpr (is-reference-wrapper-v<Maybe>) {
return m.get() ? addressof(*(m.get())) : nullptr;
} else {
return m ? addressof(*m) : nullptr;
}
8 Effects: Equivalent to:
const Maybe& m = value_.get();
if constexpr (is-reference-wrapper-v<Maybe>) {
return m.get() ? addressof(*(m.get())) : nullptr;
} else {
return m ? addressof(*m) : nullptr;
}
A pure library extension, affecting no other parts of the library or language.
[N4849] Richard Smith. 2020. Working Draft, Standard for Programming Language C++.
https://wg21.link/n4849
[P0323R9] JF Bastien, Vicente Botet. 2019. std::expected.
https://wg21.link/p0323r9
[P0896R3] Eric Niebler, Casey Carter, Christopher Di Bella. 2018. The One Ranges Proposal.
https://wg21.link/p0896r3