TIL: nullopt_t
is not equality-comparable, but monostate
is
nullopt_t
is not equality-comparable, but monostate
isOn Slack,
Kilian Henneberger asked for some STL types that are copyable but not
equality-comparable. One example is std::function<int()>
; see
“On function_ref
and string_view
” (2019-05-10).
The simplest example is struct S {};
— for compatibility with C, C++98 provided
every class type with a copy constructor, but none of them with comparison operators.
So, I said, a good STL example would be std::monostate
from the <variant>
header, which is basically just a trivial tag type, right? Wrong!
constexpr std::monostate m;
static_assert(m == m);
It turns out that monostate
needs to be totally ordered, because if it weren’t, then
variant<int, monostate>
wouldn’t be totally ordered either.
But you know what’s like a variant<int, monostate>
? optional<int>
!
Everyone knows that optional<int>
is totally ordered, right? So:
constexpr std::nullopt_t n;
bool b = (n == n); // Error: does not compile!
It turns out that std::nullopt_t
is not equality-comparable.
This makes sense in hindsight, actually. My new mental model is:
-
std::nullopt_t
is a tag type, likestd::in_place_t
or evenstd::nullptr_t
. Its only purpose is to be used in syntactic constructs likemyOptional = std::nullopt
. It’s not a “value-semantic” type; its only job is to participate in overload sets and then get out of the way as fast as possible. Anoptional
never really “holds” anullopt_t
value. In a value-semantic, Tony-van-Eerd kind of sense,nullopt_t
doesn’t have any “values.” You physically can make avector<nullopt_t>
, just like you can make avector<nullptr_t>
or avector<in_place_t>
, but you shouldn’t. Notably, you physically cannot make anoptional<nullopt_t>
. -
std::monostate
is a value-semantic type, likebool
— it just has one fewer value in its domain. It can be stored in variants, or containers, orset
s (it’s ordered!), orunordered_set
s (it’s hashable!), or anywhere else you might use a value-semantic type likebool
. It’s totally fine to make anoptional<monostate>
.
Given time, Ranges will erase this mental model
Problem: The C++20 Ranges library has Opinions about comparability. Ranges deals only with
value-semantic (or reference-semantic) types; it has very little respect for syntactic fillips
like nullopt
or nullptr
. For example, Ranges assumes that “Type X is comparable with type Y”
must necessarily mean “Values of type X are comparable with values of type Y,” i.e., X and Y
cover the same domain, i.e., common_reference_t<X, Y>
must exist and be equality-comparable with
itself.
template <class T, class U>
concept equality_comparable_with =
equality_comparable<T> &&
equality_comparable<U> &&
common_reference_with<
const remove_reference_t<T>&,
const remove_reference_t<U>&> && ~~~;
This breaks badly for syntactic tags like nullptr_t
and nullopt_t
. And because Ranges
concepts are used to constrain all the Ranges algorithms, we end up with ridicule-worthy
situations like (Godbolt):
std::unique_ptr<int> a[10];
std::optional<int> b[10];
auto it = std::find(a, a+10, nullptr); // OK
auto jt = std::find(b, b+10, std::nullopt); // OK
std::ranges::find(a, a+10, nullptr); // Error: no viable overload
std::ranges::find(a, a+10, std::nullopt); // Error: no viable overload
(By the way, if you’re not used to Concepts error messages, take a look at the error
messages in that Godbolt!) The problem is that, because nullopt_t
is not
equality_comparable
with itself (or in the other case because
const unique_ptr<int>&
is not convertible to common_reference_with<unique_ptr<int>, nullptr_t>
),
Ranges thinks that the two types are not comparable at all.
This is the subject of Justin Bassett’s recent paper
P2405 “nullopt_t
and nullptr_t
should both have operator<=>
and operator==
”
(July 2021), currently slated for “not C++23”
but maybe C++26.
In other words, the gravitational pull of Ranges will probably end up eroding
the distinction I just described between “value-semantic” types like monostate
and
“syntactic tag” types like in_place_t
and nullptr_t
, simply because Ranges cannot
(currently) deal with types that aren’t value-semantic in the most heavyweight sense possible.
Maybe this is a good thing — maybe “syntactic tag” types are an abomination and we should
be glad if they all turn into proper values. But I doubt it.