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_tis a tag type, likestd::in_place_tor 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. Anoptionalnever really “holds” anullopt_tvalue. In a value-semantic, Tony-van-Eerd kind of sense,nullopt_tdoesn’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::monostateis a value-semantic type, likebool— it just has one fewer value in its domain. It can be stored in variants, or containers, orsets (it’s ordered!), orunordered_sets (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.
