Effect of operator<=> on the C++ Standard Library

This paper lists (what are expected to be) non-controversial changes to the C++ standard library in response to P0515, which adds operator<=> to the language. This is expected to be non-controversial because it tries to match existing behavior as much as possible. As a result, all proposed additions are either strong_equality or strong_ordering, matching the existing comparison operators.

This document should contain a complete list of types or categories of types in C++.

Revision History

R1: A much broader version of this paper was presented to LEWG at a previous meeting. What remains in this paper is everything which the group did not find controversial and which probably does not require significant justification. All controversial aspects will be submitted in separate papers.
R2: Added wording.

Backward Compatibility

The operator<=> proposal was written such that the "generated" operators are equivalent to source code rewrites – there is no actual operator== that a user could take the address of. Users are not allowed to form pointers to standard library member functions and are unable to form pointers to friend functions defined inline in the class. There are some cases where we do not specify how the operator was implemented, only that the expression a @ b is valid; these cases are not broken by such a change because users could not have depended on it, anyway. In general, we accept changes that overload existing functions, which also has the effect of breaking code which takes the address of a free function.

Types that are not proposed to get operator<=> in this paper

These types are not comparable now. This paper does not propose adding any new comparisons to any of these types.

deprecated types

exception types

tag classes (nothrow, piecewise_construct_t, etc.)

arithmetic function objects (plus, minus, etc.)

comparison function objects (equal_to, etc.)

owner_less

logical function objects (logical_and, etc.)

bitwise function objects (bit_and, etc.)

nested_exception

allocator_traits

pool_options

char_traits

iterator_traits

numeric_limits

pointer_traits

regex_traits

chrono::duration_values

tuple_element

max_align_t

map::node_type

map::insert_return_type

set::node_type

set::insert_return_type

unordered_map::node_type

unordered_map::insert_return_type

unordered_set::node_type

unordered_set::insert_return_type

any

default_delete

aligned_storage

aligned_union

system_clock

steady_clock

high_resolution_clock

locale::facet

locale::id

ctype_base

ctype

ctype_byname

codecvt_base

codecvt

codecvt_byname

num_get

num_put

numpunct

numpunct_byname

collate

collate_byname

time_get

time_get_byname

time_put

time_put_byname

money_base

money_get

money_put

money_punct

moneypunct_byname

message_base

messages

messages_byname

FILE

va_list

back_insert_iterator

front_insert_iterator

insert_iterator

ostream_iterator

ostreambuf_iterator

ios_base

ios_base::Init

basic_ios

basic_streambuf

basic_istream

basic_iostream

basic_ostream

basic_stringbuf

basic_istringstream

basic_ostringstream

basic_stringstream

basic_filebuf

basic_ifstream

basic_ofstream

basic_fstream

gslice

slice_array

gslice_array

mask_array

indirect_array

atomic_flag

thread

mutex

recursive_mutex

timed_mutex

timed_recursive_mutex

lock_guard

scoped_lock

unique_lock

once_flag

shared_mutex

shared_timed_mutex

shared_lock

condition_variable

condition_variable_any

promise

future

shared_future

packaged_task

random_device

hash

weak_ptr

basic_regex

sequential_execution_policy

parallel_execution_policy

parallel_vector_execution_policy

default_searcher

boyer_moore_searcher

boyer_moore_horspool_searcher

ratio

integer_sequence

seed_seq (paper needed to add strong_equality)

enable_shared_from_this: It would be nice to give it a strong_ordering to allow derived classes to = default. However, this means that all classes that do not explicitly delete their comparison operator get an operator<=> that compares only the enable_shared_from_this base class, which is almost certainly wrong. Since this is intended to be used as a base class, we should not add operator<=> to it. Moreover, classes which enable_shared_from_this are unlikely to be basic value classes so they do not lose much by not being able to default.

initializer_list: initializer_list is a reference type. It would be strange to give it reference semantics on copy but value semantics for comparison. It would also be surprising if two initializer_list containing the same set of values compared as not equal. Therefore, I recommend not defining it for this type.

Types that should get <=> with a return type of strong_ordering, no change from current comparisons

These types are all currently comparable.

error_category

error_code

error_condition

exception_ptr

monostate

chrono::duration: heterogeneous with durations of other representations and periods

chrono::time_point: heterogeneous in the duration

type_index

filesystem::path

filesystem::directory_entry

thread::id

array::iterator

deque::iterator

vector::iterator

valarray::iterator

Types that will get their operator<=> from a conversion operator

These types will get operator<=> if possible without any changes, just like they already have whatever comparison operators their underlying type has.

integral_constant and all types deriving from integral_constant (has operator T)

bitset::reference (has operator bool)

reference_wrapper (has operator T &)

atomic (has operator T)

This has the disadvantage that types which have a template comparison operator will not have their wrapper convertible. For instance, std::reference_wrapper<std::string> is not currently comparable. This does not affect bitset::reference, as it has a fixed conversion to bool, but it does affect the other three.

Types that wrap another type

array

deque

forward_list

list

vector (including vector<bool>)

map

set

multimap

multiset

unordered_map

unodered_set

unodered_multimap

unordered_multiset

queue

queue::iterator

priority_queue

priority_queue::iterator

stack

stack::iterator

pair

tuple

reverse_iterator

move_iterator

optional

variant

This turned out to be much more complicated than expected and will require its own paper.

basic_string, basic_string_view, char_traits, and sub_match

Properly integrating operator<=> with these types requires more thought than this paper has room for, and thus will be discussed separately.

unique_ptr and shared_ptr

They contain state that is not observed in the comparison operators. Therefore, they will get their own paper.

valarray

Current comparison operators return a valarray<bool>, giving you the result for each pair (with undefined behavior for differently-sized valarray arguments). It might make sense to provide some sort of function that returns valarray<comparison_category>, but that should not be named operator<=>. This paper does not suggest adding operator<=> to valarray.

Types that have no comparisons now but are being proposed to get operator<=> in another paper

This paper does not propose changing any of the following types -- they are here only for completeness.

filesystem::file_status

filesystem::space_info

slice

to_chars_result

from_chars_result

nullptr_t

Already supports strong_equality in the working draft. I will be writing a separate paper proposing strong_ordering.

Not Updating Concepts That Provide Comparisons

This category includes things like BinaryPredicate and Compare. This is addressed in a separate paper.

Not Updating Concepts That Require Comparisons

This includes things like LessThanComparable and EqualityComparable. This is addressed in a separate paper.

Miscellaneous

All operator<=> should be constexpr and noexcept where possible, following the lead of the language feature and allowing = default as an implementation strategy for some types.

When we list a result type as "unspecified" it is unspecified whether it has operator<=>. There are not any unspecified result types for which we currently guarantee any comparison operators are present, so there is no extra work to do here.

Wording

General wording

15.4.2.3 Operators [operators]

1 In this library, whenever a declaration is provided for an operator!=, operator>, operator<=, or operator>= for a type T, its requirements and semantics are as follows, unless explicitly specified otherwise.

bool operator!=(const T& x, const T& y);

2 Requires: Type T is Cpp17EqualityComparable (Table 23).

3 Returns: !(x == y).

bool operator>(const T& x, const T& y);

4 Requires: Type T is Cpp17LessThanComparable (Table 24).

5 Returns: y < x.

bool operator<=(const T& x, const T& y);

6 Requires: Type T is Cpp17LessThanComparable (Table 24).

7 Returns: !(y < x).

bool operator>=(const T& x, const T& y);

8 Requires: Type T is Cpp17LessThanComparable (Table 24).

9 Returns: !(x < y).

1 Unless specified otherwise, if lhs and rhs are values of types from this library, the following shall hold:

lhs != rhs is a valid expression if and only if lhs == rhs is a valid expression.

lhs < rhs, lhs > rhs, lhs <= rhs, and lhs >= rhs are all valid expressions if and only if lhs <=> rhs is a valid expression.

If lhs <=> rhs is a valid expression, lhs == rhs is a valid expression.

The requirements and semantics of these operators are as follows, unless explicitly specified otherwise.

1 For an object index of type type_index, hash<type_index>()(index) shall evaluate to the same result as index.hash_code().

19.17.5 Comparisons [type.index.comparisons]

For two values lhs and rhs of type type_index, the expression lhs <=> rhs is of type std::strong_­ordering. If *lhs.target == *rhs.target, lhs <=> rhs yields std::strong_­ordering::equal; if lhs and rhs compare unequal, lhs <=> rhs yields std::strong_­ordering::less if lhs.target->before(*rhs.target) is true, otherwise yields std::strong_ordering::greater. [Note: A result of std::strong_ordering::greater happens only in the case where rhs.target->before(*lhs.target) is true. — end note]

7 Returns: !(lhs < rhs).
For two values lhs and rhs, each of which are instances of the class template std::chrono::duration (with possibly different Rep and Period template parameters), the expression lhs <=> rhs is equivalent to CT(lhs).count() <=> CT(rhs).count(). [Note: This implies the operation is constexpr if both lhs and rhs are constexpr. — end note]

6 Returns: !(lhs < rhs).
For two values lhs and rhs, each of which are instances of the class template std::chrono::time_point (with possibly the same Clock template parameter but possibly different Duration template parameters), the expression lhs <=> rhs is equivalent to CT(lhs).time_since_epoch() <=> CT(rhs).time_since_epoch(). [Note: This implies the operation is constexpr if both lhs and rhs are constexpr. — end note]

For two values lhs and rhs of type std::filesystem::path, the expression lhs <=> rhs is equivalent to lhs.compare(rhs) <=> 0

28.11.7.7 Non-member functions [fs.path.nonmember]

void swap(path& lhs, path& rhs) noexcept;

1 Effects: Equivalent to lhs.swap(rhs).

size_t hash_value (const path& p) noexcept;

2 Returns: A hash value for the path p. If for two paths, p1 == p2 then hash_value(p1) == hash_value(p2).

friend bool operator==(const path& lhs, const path& rhs) noexcept;

3 Returns: !(lhs < rhs) && !(rhs < lhs).

4 [Note: Path equality and path equivalence have different semantics.
— (4.1) Equality is determined by the path non-member operator==, which considers the two paths’ lexical representations only. [Example: path("foo") == "bar" is never true. — end example]
— (4.2) Equivalence is determined by the equivalent() non-member function, which determines if two paths resolve (28.11.7) to the same file system entity. [Example: equivalent("foo", "bar") will be true when both paths resolve to the same file. — end example]

Programmers wishing to determine if two paths are “the same” must decide if “the same” means
“the same representation” or “resolve to the same actual file”, and choose the appropriate function
accordingly. — end note]

1 An object of type thread::id provides a unique identifier for each thread of execution and a single distinct value for all thread objects that do not represent a thread of execution (31.3.2). Each thread of execution has an associated thread::id object that is not equal to the thread::id object of any other thread of execution and that is not equal to the thread::id object of any thread object that does not represent threads of execution.

2 thread::id is a trivially copyable class (10.1). The library may reuse the value of a thread::id of a terminated thread that can no longer be joined.

3 [Note: Relational operators allow thread::id objects to be used as keys in associative containers. — end note]

id() noexcept;

4 Effects: Constructs an object of type id.

5 Ensures: The constructed object does not represent a thread of execution.

bool operator==(thread::id x, thread::id y) noexcept;

6 Returns: true only if x and y represent the same thread of execution or neither x nor y represents a thread of execution.

bool operator!=(thread::id x, thread::id y) noexcept;

7 Returns: !(x == y)

bool operator<(thread::id x, thread::id y) noexcept;

8 Returns: A value such that operator< is a total ordering as described in 24.7.

126 Effects: Inserts an unspecified text representation of id into out. For two objects of type thread::idx and y, if x == y the thread::id objects have the same text representation and if x != y the thread::id objects have distinct text representations.

137 Returns: out.

template<> struct hash<thread::id>;

148 The specialization is enabled (19.14.18).

For two values lhs and rhs of type std::thread::id, the expression lhs <=> rhs is of type std::strong_­ordering. If lhs and rhs represent the same thread of execution or neither x nor y represents a thread of execution, lhs <=> rhs yields std::strong_­ordering::equal; if lhs and rhs compare unequal, lhs <=> rhs yields a value consistent with a total order as described in 24.7.