TIL: `nullopt_t` is not equality-comparable, but `monostate` is – Arthur O'Dwyer...
source link: https://quuxplusone.github.io/blog/2022/03/07/monostate-vs-nullopt/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
TIL: nullopt_t
is not equality-comparable, but monostate
is
On 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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK