In writing range transformation pipelines 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 view::single fills a similar purpose for non-nullable values, lifting a single value into a view, and view::empty provides a range of no values of a given type.
A view::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 = view::join(view::transform(v, view::maybe));
for (auto i : r) {
std::cout << i; // prints 42 and 42^H^H 56
}
std::optional o{7};
for (auto&& i : view::maybe(o)) {
i = 9;
std::cout << "i=" << i << " prints 9\n";
}
std::cout << "o=" << *o << " prints 9\n";
auto oe = std::optional<int>{};
for (int i : view::maybe(oe))
std::cout << "i=" << i << '\n'; // does not print
Depending on if the underlying nullable is a temporary or not, the view either captures a copy of the nullable object in a semiregular-box
or holds a pointer to the underlying object. In either case, if the nullable object is empty, data()
returns nullptr, otherwise the address of the value.
The view interface function size()
returns 0 or 1, and begin()
and end()
have natural definitions in terms of data()
and size()
.
struct __maybe_fn {
template <class T, Nullable U = std::remove_cv_t<T>>
requires ranges::Constructible<U, T> &&
ranges::CopyConstructible<U>
constexpr safe_maybe_view<U> operator()(T&& t)
const noexcept(std::is_nothrow_constructible_v<U, T>) {
return safe_maybe_view<U>{std::move(t)}; // Safe copy
}
template <Nullable T>
constexpr ref_maybe_view<T> operator()(T& t)
const noexcept {
return ref_maybe_view<T>{t}; // Refer to t
}
};
inline constexpr __maybe_fn maybe{};
Some details removed
template <Nullable Maybe>
requires ranges::CopyConstructible<Maybe>
class safe_maybe_view
: public ranges::view_interface<safe_maybe_view<Maybe>> {
private:
using T = remove_reference_t<ranges::iter_reference_t<Maybe>>;
ranges::detail::semiregular_box<Maybe> value_;
public:
constexpr safe_maybe_view() = default;
constexpr explicit safe_maybe_view(Maybe&& maybe)
noexcept(is_nothrow_move_constructible_v<Maybe>)
: value_(move(maybe)) {}
constexpr T* begin() noexcept { return data(); }
constexpr T* end() noexcept { return data() + size(); }
constexpr ptrdiff_t size() const noexcept { return bool(value_.get()); }
constexpr T* data() noexcept {
Maybe& m = value_.get();
return m ? addressof(*m) : nullptr;
}
};
Some details removed
template <Nullable Maybe>
class ref_maybe_view
: public std::experimental::ranges::view_interface<ref_maybe_view<Maybe>> {
using T = std::remove_reference_t<ranges::iter_reference_t<Maybe>>;
Maybe* value_ = nullptr;
public:
constexpr ref_maybe_view() = default;
constexpr explicit ref_maybe_view(Maybe& maybe) noexcept
: value_(std::addressof(maybe)) {}
constexpr T* begin() noexcept { return data(); }
constexpr T* end() noexcept { return data() + size(); }
constexpr std::ptrdiff_t size() const noexcept { return bool(*value_); }
constexpr T* data() noexcept {
return *value_ ? std::addressof(**value_) : nullptr;
}
};
Leaving out equality preserving requirements
template <class T>
concept bool Nullable =
std::is_object_v<T> &&
requires(T& t, const T& ct) {
bool(ct);
*t;
*ct;
};
Presented R0 at San Diego
“Not Small”
Encouraged to make it small.
Internally to Bloomberg got assurances that our nullable type would be made to conform with the std nullable protocol.
Removed all customization points.
Now defined purely in terms of the, non-normative, Nullable Concept.
A Nullalble is Contextually Bool and Dereferenceable.
Considered Readable, instead of dereferenceable, but that pulls in expensive traits.
Discussed in San Diego: http://wiki.edg.com/bin/view/Wg21sandiego2018/P1255
SF | F | N | A | SA |
6 | 7 | 5 | 1 | 0 |
like the Maybe concept (contextually convertible to bool not by decay, dereferencable to object type)
SF | F | N | A | SA |
2 | 12 | 1 | 1 | 0 |
SV | V | N | O | SO |
6 | 7 | 3 | 2 | 0 |
Ideally C++20, failing that, C++23 and get it into experimental/ in range libraries.
Similar facilities in wide use in functional languages, for lifting an Optional type into List.
A simplified version used in Eric Niebler’s Pythagorian Triples, Revisited
// maybe_view defines a view over zero or one
// objects.
template<Semiregular T>
struct maybe_view : view_interface<maybe_view<T>> {
maybe_view() = default;
maybe_view(T t) : data_(std::move(t)) {
}
T const *begin() const noexcept {
return data_ ? &*data_ : nullptr;
}
T const *end() const noexcept {
return data_ ? &*data_ + 1 : nullptr;
}
private:
optional<T> data_{};
};
Implementation available at https://github.com/steve-downey/view_maybe which depends on cmcstl2.
https://github.com/steve-downey/view_maybe/blob/master/src/view_maybe/view_maybe.h
There will be a PR for CMCSTL2 soon. There was red tape involved.
This tends to be written, poorly, by many people implementing range style code.