A reunião em Colônia passou, o padrão C ++ 20 foi reduzido para uma aparência mais ou menos finalizada (pelo menos até o aparecimento de notas especiais), e eu gostaria de falar sobre uma das inovações futuras. Este é um mecanismo que geralmente é chamado de
operador <=> (o padrão o define como um "operador de comparação de três vias", mas tem o apelido informal de "nave espacial"), mas acredito que seu escopo seja muito mais amplo.
Não teremos apenas um novo operador - a semântica das comparações sofrerá mudanças significativas no nível da própria linguagem.
Mesmo que você não consiga tirar mais nada deste artigo, lembre-se desta tabela:
Agora teremos um novo operador,
<=> , mas, mais importante, os operadores estão sistematizados. Existem operadores básicos e operadores derivativos - cada grupo tem seus próprios recursos.
Falaremos sobre esses recursos brevemente na introdução e consideraremos mais detalhadamente nas próximas seções.
Os operadores básicos podem ser
invertidos (ou seja, reescritos com a ordem inversa dos parâmetros). As instruções derivadas podem ser
reescritas através da instrução base correspondente. Os candidatos convertidos ou reescritos não geram novas funções, eles são simplesmente substituições no nível do código-fonte e são selecionados de um
conjunto estendido de candidatos . Por exemplo, a expressão
a <9 agora pode ser avaliada como
a.operator <=> (9) <0 , e a expressão
10! = B como
! Operator == (b, 10) . Isso significa que será possível dispensar um ou dois operadores em que, para atingir o mesmo comportamento, agora é necessário escrever manualmente 2, 4, 6 ou até 12 operadores.
Uma breve visão geral das regras será apresentada abaixo, juntamente com uma tabela de todas as transformações possíveis.
Os operadores básico e derivativo podem ser definidos como
padrão . No caso de operadores básicos, isso significa que o operador será aplicado a cada membro na ordem de declaração; no caso de operadores derivados, esses candidatos reescritos serão usados.
Deve-se notar que não existe tal transformação na qual um operador de um tipo (isto é, igualdade ou ordem) possa ser expresso em termos de um operador de outro tipo. Em outras palavras, as colunas da nossa tabela não dependem uma da outra. A expressão
a == b nunca será avaliada como
operador <=> (a, b) == 0 implicitamente (mas, é claro, nada impede que você defina seu
operador == usando o
operador <=>, se desejar).
Considere um pequeno exemplo no qual mostramos como o código fica antes e depois de aplicar a nova funcionalidade. Escreveremos um tipo de string que não
diferencia maiúsculas de minúsculas,
CIString , cujos objetos podem ser comparados entre si e com
char const * .
No C ++ 17, para nossa tarefa, precisamos escrever 18 funções de comparação:
class CIString { string s; public: friend bool operator==(const CIString& a, const CIString& b) { return assize() == bssize() && ci_compare(asc_str(), bsc_str()) == 0; } friend bool operator< (const CIString& a, const CIString& b) { return ci_compare(asc_str(), bsc_str()) < 0; } friend bool operator!=(const CIString& a, const CIString& b) { return !(a == b); } friend bool operator> (const CIString& a, const CIString& b) { return b < a; } friend bool operator>=(const CIString& a, const CIString& b) { return !(a < b); } friend bool operator<=(const CIString& a, const CIString& b) { return !(b < a); } friend bool operator==(const CIString& a, const char* b) { return ci_compare(asc_str(), b) == 0; } friend bool operator< (const CIString& a, const char* b) { return ci_compare(asc_str(), b) < 0; } friend bool operator!=(const CIString& a, const char* b) { return !(a == b); } friend bool operator> (const CIString& a, const char* b) { return b < a; } friend bool operator>=(const CIString& a, const char* b) { return !(a < b); } friend bool operator<=(const CIString& a, const char* b) { return !(b < a); } friend bool operator==(const char* a, const CIString& b) { return ci_compare(a, bsc_str()) == 0; } friend bool operator< (const char* a, const CIString& b) { return ci_compare(a, bsc_str()) < 0; } friend bool operator!=(const char* a, const CIString& b) { return !(a == b); } friend bool operator> (const char* a, const CIString& b) { return b < a; } friend bool operator>=(const char* a, const CIString& b) { return !(a < b); } friend bool operator<=(const char* a, const CIString& b) { return !(b < a); } };
No C ++ 20, você pode executar apenas 4 funções:
class CIString { string s; public: bool operator==(const CIString& b) const { return s.size() == bssize() && ci_compare(s.c_str(), bsc_str()) == 0; } std::weak_ordering operator<=>(const CIString& b) const { return ci_compare(s.c_str(), bsc_str()) <=> 0; } bool operator==(char const* b) const { return ci_compare(s.c_str(), b) == 0; } std::weak_ordering operator<=>(const char* b) const { return ci_compare(s.c_str(), b) <=> 0; } };
Vou lhe dizer o que isso significa, com mais detalhes, mas primeiro, vamos voltar um pouco e lembrar como as comparações funcionaram com o padrão C ++ 20.
Comparações em padrões de C ++ 98 a C ++ 17
As operações de comparação não mudaram muito desde a criação do idioma. Tínhamos seis operadores:
== ,! = ,
< ,
> ,
<= E
> = . O padrão define cada um deles para tipos internos, mas em geral eles obedecem às mesmas regras. Ao avaliar qualquer expressão
a @ b (onde
@ é um dos seis operadores de comparação), o compilador procura funções-membro, funções livres e candidatos internos chamados
operador @ , que podem ser chamados com o tipo
A ou
B na ordem especificada. O candidato mais adequado é selecionado a partir deles. Isso é tudo. De fato,
todos os operadores trabalharam da mesma maneira: a operação
< não difere de
<< .
Um conjunto tão simples de regras é fácil de aprender. Todos os operadores são absolutamente independentes e equivalentes. Não importa o que nós, humanos, sabemos sobre a relação fundamental entre
== e
! = Operações. Em termos de idioma, esse é o mesmo. Nós usamos expressões idiomáticas. Por exemplo, definimos o operador
! = Through
== :
bool operator==(A const&, A const&); bool operator!=(A const& lhs, A const& rhs) { return !(lhs == rhs); }
Da mesma forma, através do operador
< , definimos todos os outros operadores de relação. Usamos esses idiomas porque, apesar das regras do idioma, não consideramos realmente os seis operadores equivalentes. Aceitamos que dois deles sejam básicos (
== e
< ), e através deles todos os outros já estão expressos.
De fato, a Biblioteca de modelos padrão é construída inteiramente sobre esses dois operadores, e o grande número de tipos no código explorado contém definições de apenas um deles ou ambos.
No entanto, o operador
< não é muito adequado para a função base por dois motivos.
Primeiro, não é possível garantir que outros operadores de relacionamento se expressem através dele. Sim,
a> b significa exatamente o mesmo que
b <a , mas não é verdade que
a <= b significa exatamente o mesmo que
! (B <a) . As duas últimas expressões serão equivalentes se houver uma propriedade de tricotomia, na qual, para quaisquer dois valores, apenas uma das três afirmações é verdadeira:
a <b ,
a == b ou
a> b . Na presença de uma tricotomia, a expressão
a <= b significa que estamos lidando com o primeiro ou o segundo caso ... e isso é equivalente à afirmação de que não estamos lidando com o terceiro caso. Portanto
(a <= b) ==! (A> b) ==! (B <a) .
Mas e se a atitude não possuir a propriedade da tricotomia? Isso é característico das relações parciais de ordem. Um exemplo clássico são números de ponto flutuante para os quais qualquer uma das operações
1.f <NaN ,
1.f == NaN e
1.f> NaN fornece
false . Portanto,
1.f <= NaN também é
mentira , mas ao mesmo tempo
! (NaN <1.f) é
verdadeiro .
A única maneira de implementar o operador
<= em termos gerais por meio dos operadores básicos é pintar as duas operações como
(a == b) || (a <b) , que é um grande passo para trás, se
ainda tivermos que lidar com a ordem linear, desde então, nenhuma função será chamada, mas duas (por exemplo, a expressão
"abc..xyz9" <= "abc ..xyz1 " terá que ser reescrito como
(" abc..xyz9 "==" abc..xyz1 ") || (" abc..xyz9 "<" abc..xyz1 ") e duas vezes para comparar a linha inteira).
Em segundo lugar, o operador
< não
é muito adequado para o papel básico devido às peculiaridades de seu uso em comparações lexicográficas. Os programadores costumam cometer este erro:
struct A { T t; U u; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u; } bool operator< (A const& rhs) const { return t < rhs.t && u < rhs.u; } };
Para definir o operador == para uma coleção de elementos, basta aplicar
== a cada membro uma vez, mas isso não funcionará com o operador
< . Do ponto de vista desta implementação, os conjuntos
A {1, 2} e
A {2, 1} serão considerados equivalentes (já que nenhum deles é menor que o outro). Para corrigir isso, aplique o operador
< duas vezes a cada membro, exceto o último:
bool operator< (A const& rhs) const { if (t < rhs.t) return true; if (rhs.t < t) return false; return u < rhs.u; }
Por fim, para garantir a operação correta das comparações de objetos heterogêneos - ou seja, para garantir que as expressões
a == 10 e
10 == signifiquem a mesma coisa - elas geralmente recomendam escrever comparações como funções livres. De fato, essa é geralmente a única maneira de implementar essas comparações. Isso é inconveniente porque, em primeiro lugar, você precisa monitorar a conformidade com esta recomendação e, em segundo lugar, geralmente precisa declarar essas funções como amigos ocultos para uma implementação mais conveniente (ou seja, dentro do corpo da classe).
Observe que, ao comparar objetos de tipos diferentes, nem sempre é necessário escrever
operator == (X, int) ; eles também podem significar casos em que
int pode ser convertido para
X implicitamente.
Vamos resumir as regras para o padrão C ++ 20:
- Todas as instruções são tratadas da mesma maneira.
- Usamos expressões idiomáticas para facilitar a implementação. Os operadores == e < adotamos os idiomas básicos e expressamos os operadores de relacionamento restantes por meio deles.
- Isso é apenas o operador < não é muito adequado para o papel da base.
- É importante (e recomendado) escrever comparações de objetos heterogêneos como funções livres.
Novo operador de pedidos básicos: <=>
A mudança mais significativa e perceptível no trabalho de comparações no C ++ 20 é a adição de um novo operador -
operador <=> , um operador de comparação de três vias.
Já estamos familiarizados com comparações de três vias pelas funções
memcmp /
strcmp em C e
basic_string :: compare () em C ++. Todos eles retornam um valor do tipo
int , que é representado por um número positivo arbitrário se o primeiro argumento for maior que o segundo,
0 se forem iguais e um número negativo arbitrário caso contrário.
O operador "nave espacial" não retorna um valor
int , mas um objeto pertencente a uma das categorias de comparação, cujo valor reflete o tipo de relacionamento entre os objetos comparados. Existem três categorias principais:
- strong_ordering : uma relação de ordem linear na qual igualdade implica a intercambiabilidade de elementos (ou seja, (a <=> b) == strong_ordering :: equal implica que f (a) == f (b) é válido para todas as funções adequadas f O termo "função adequada" intencionalmente não recebe uma definição clara, mas não inclui funções que retornam os endereços de seus argumentos ou a capacidade () do vetor, etc. Estamos interessados apenas nas propriedades "essenciais", que também são muito vagas, mas condicionalmente possíveis Assuma que estamos falando sobre o valor do tipo. O valor do vetor está contido nele m elementos, mas não o endereço dele etc.). Esta categoria inclui os seguintes valores: strong_ordering :: maior , strong_ordering :: equal e strong_ordering :: less .
- fraca_ordenação : uma relação de ordem linear na qual igualdade define apenas uma certa classe de equivalência. Um exemplo clássico é a comparação de cadeias sem distinção entre maiúsculas e minúsculas, quando dois objetos podem ser fraco_ordering :: equivalente , mas não são estritamente iguais (isso explica a substituição da palavra igual por equivalente no nome do valor).
- ordem_ parcial: relação de ordem parcial. Nesta categoria, mais um valor é adicionado aos valores maior , equivalente e menor (como na ordem fraca ) - não ordenado ("desordenado"). Pode ser usado para expressar relações parciais de ordem em um sistema de tipos: 1.f <=> NaN fornece o valor initial_ordering :: unordered .
Você trabalhará principalmente com a categoria
strong_ordering ; Essa também é a categoria ideal para uso por padrão. Por exemplo,
2 <=> 4 retorna
strong_ordering :: less e
3 <=> -1 retorna strong_ordering :: maior .
Categorias de uma ordem superior podem ser implicitamente reduzidas a categorias de uma ordem mais fraca (ou seja, a ordem
forte é redutível a ordem
fraca ). Nesse caso, o tipo atual de relacionamento é preservado (ou seja,
strong_ordering :: equal se transforma em
fraco_ordering :: equivalente ).
Os valores das categorias de comparação podem ser comparados com o literal
0 (não com nenhum
int e não com
int igual a
0 , mas simplesmente com o literal
0 ) usando um dos seis operadores de comparação:
strong_ordering::less < 0
É graças a uma comparação com o literal
0 que podemos implementar os operadores de relação:
a @ b é equivalente a
(a <=> b) @ 0 para cada um desses operadores.
Por exemplo,
2 <4 pode ser calculado como
(2 <=> 4) <0 , que se transforma em
strong_ordering :: less <0 e fornece o valor
true .
O operador
<=> se ajusta muito melhor ao papel do elemento base que o operador
< , pois elimina os dois problemas do último.
Primeiro, garante-se que a expressão
a <= b seja equivalente a
(a <=> b) <= 0, mesmo com ordenação parcial. Para dois valores não ordenados,
a <=> b fornecerá o valor
parcial_ordered :: unordered e
parcial_ordered :: unordered <= 0 fornecerá
false , que é o que precisamos. Isso é possível porque
<=> pode retornar mais variedades de valores: por exemplo, a categoria de
parcial_ordering contém quatro valores possíveis. Um valor do tipo
bool só pode ser
verdadeiro ou
falso , portanto, antes não era possível distinguir entre comparações de valores ordenados e não ordenados.
Para maior clareza, considere um exemplo de um relacionamento de ordem parcial que não está relacionado aos números de ponto flutuante. Suponha que desejemos adicionar um estado NaN a um tipo
int , em que NaN é apenas um valor que não forma um par ordenado com qualquer valor envolvido. Você pode fazer isso usando
std :: optional para armazená-lo:
struct IntNan { std::optional<int> val = std::nullopt; bool operator==(IntNan const& rhs) const { if (!val || !rhs.val) { return false; } return *val == *rhs.val; } partial_ordering operator<=>(IntNan const& rhs) const { if (!val || !rhs.val) {
O operador
<= retorna o valor correto, porque agora podemos expressar mais informações no nível do próprio idioma.
Em segundo lugar, para obter todas as informações necessárias, basta aplicar
<=> uma vez, o que facilita a implementação da comparação lexicográfica:
struct A { T t; U u; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u; } strong_ordering operator<=>(A const& rhs) const {
Veja
P0515 , a frase original para adicionar o
operador <=>, para uma discussão mais detalhada
.Novos recursos do operador
Não temos apenas um novo operador à nossa disposição. No final, se o exemplo mostrado acima com a declaração da estrutura
A disser apenas que, em vez de
x <y, agora precisamos escrever
(x <=> y) <0 toda vez, ninguém gostaria disso.
O mecanismo para resolver comparações no C ++ 20 difere significativamente da abordagem antiga, mas essa alteração está diretamente relacionada ao novo conceito de dois operadores básicos de comparação:
== e
<=> . Se antes era um idioma (gravando via
== e
< ), que usamos, mas que o compilador não sabia, agora ele entenderá essa diferença.
Mais uma vez, darei a tabela que você já viu no início do artigo:
Cada um dos operadores básicos e derivativos recebeu uma nova habilidade, que vou dizer mais algumas palavras.
Inversão de operadores básicos
Como exemplo, considere um tipo que só pode ser comparado com
int :
struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } };
Do ponto de vista das regras antigas, não surpreende que a expressão
a == 10 funcione e seja avaliada como
a.operator == (10) .
Mas e quanto a
10 == a ? No C ++ 17, essa expressão seria considerada um erro de sintaxe claro. Não existe esse operador. Para que esse código funcione, você teria que escrever um
operador simétrico
== , que pegaria primeiro o valor de
int e depois
A ... e seria necessário implementá-lo como uma função livre.
No C ++ 20, os operadores básicos podem ser invertidos. Para
10 == a, o compilador encontrará o
operador candidato
== (A, int) (na verdade, esta é uma função-membro, mas para maior clareza, eu a escrevo aqui como uma função livre) e, adicionalmente - uma variante com a ordem inversa dos parâmetros, ou seja, .
operador == (int, A) . Este segundo candidato coincide com a nossa expressão (e, idealmente), por isso a escolheremos. A expressão
10 == a em C ++ 20 é avaliada como
a.operator == (10) . O compilador entende que a igualdade é simétrica.
Agora vamos expandir nosso tipo para que ele possa ser comparado com
int não apenas pelo operador de igualdade, mas também pelo operador de pedidos:
struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } strong_ordering operator<=>(int j) const { return i <=> j; } };
Novamente, a expressão
a <=> 42 funciona bem e é calculada de acordo com as regras antigas como
a.operator <=> (42) , mas
42 <=> a estaria errado do ponto de vista do C ++ 17, mesmo se o operador
< => já existia no idioma. Mas no C ++ 20, o
operador <=> , como o
operador == , é simétrico: reconhece candidatos invertidos. Para
42 <=> a, um
operador de função membro
<=> (A, int) será encontrado (novamente, eu o escrevo aqui como uma função livre apenas para maior clareza), assim como um
operador candidato sintético
<=> (int, A) . Esta versão invertida corresponde exatamente à nossa expressão - nós a selecionamos.
No entanto,
42 <=> a NÃO
é calculado como
um.operador <=> (42) . Isso seria errado. Esta expressão é avaliada como
0 <=> a.operator <=> (42) . Tente descobrir por que essa entrada está correta.
É importante observar que o compilador não cria novas funções. Ao calcular
10 == a , o novo operador
operador == (int, A) não apareceu e, ao calcular
42 <=> a , o
operador <=> (int, A) não apareceu. Apenas duas expressões são reescritas por meio de candidatos invertidos. Repito: nenhuma nova função é criada.
Observe também que um registro com a ordem inversa dos parâmetros está disponível apenas para operadores básicos, mas para derivativos não. Isto é:
struct B { bool operator!=(int) const; }; b != 42;
Reescrevendo Operadores Derivados
Vamos voltar ao nosso exemplo com a estrutura
A :
struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } strong_ordering operator<=>(int j) const { return i <=> j; } };
Tome a expressão
a! = 17 . No C ++ 17, esse é um erro de sintaxe porque o
operador! = Operator não existe. No entanto, no C ++ 20, para expressões que contêm operadores de comparação de derivadas, o compilador também procurará os operadores básicos correspondentes e expressará comparações de derivadas através deles.
Sabemos que, em matemática, a operação
! = Essencialmente significa NÃO
== . Agora isso é conhecido pelo compilador. Para a expressão
a! = 17, ele procurará não apenas o
operador! = Operadores , mas também o
operador == (e, como nos exemplos anteriores, o
operador invertido
== ). Para este exemplo, encontramos um operador de igualdade que quase nos convém - só precisamos reescrevê-lo de acordo com a semântica desejada:
a! = 17 será calculado como
! (A == 17) .
Da mesma forma,
17! = A é calculado como
! A.operator == (17) , que é uma versão reescrita e invertida.
Transformações semelhantes também são realizadas para pedidos de operadores. Se escrevêssemos
um <9 , tentaríamos (sem êxito) encontrar o
operador < , e também consideraríamos os candidatos básicos:
operator <=> . A substituição correspondente para os operadores de relação é assim:
a @ b (onde
@ é um dos operadores de relação) é calculado como
(a <=> b) @ 0 . No nosso caso,
a.operator <=> (9) <0 . Da mesma forma,
9 <= a é calculado como
0 <= a.operator <=> (9) .
Observe que, como no caso da chamada, o compilador não cria novas funções para os candidatos reescritos. Eles são simplesmente calculados de maneira diferente e todas as transformações são realizadas apenas no nível do código-fonte.
O exposto acima me leva ao seguinte conselho:
SOMENTE OPERADORES BÁSICOS : Defina apenas operadores básicos (== e <=>) no seu tipo.Como os operadores básicos fornecem todo o conjunto de comparações, basta defini-las. Isso significa que você precisa de apenas 2 operadores para comparar objetos do mesmo tipo (em vez de 6, a partir de agora) e de apenas 2 operadores para comparar diferentes tipos de objetos (em vez de 12). Se você precisar apenas da operação de igualdade, basta escrever 1 função para comparar objetos do mesmo tipo (em vez de 2) e 1 função para comparar diferentes tipos de objetos (em vez de 4). A
classe std :: sub_match é um caso extremo: no C ++ 17, ele usa 42 operadores de comparação e no C ++ 20, usa apenas 8, enquanto a funcionalidade não sofre de forma alguma.
Como o compilador também considera candidatos invertidos, todos esses operadores podem ser implementados como funções de membro. Você não precisa mais escrever funções gratuitas apenas para comparar objetos de tipos diferentes.
Regras especiais para encontrar candidatos
Como já mencionei, a busca de candidatos para
a @ b em C ++ 17 foi realizada de acordo com o seguinte princípio: encontramos todos os
operadores operator @ e selecionamos o
operador mais adequado.
O C ++ 20 usa um conjunto estendido de candidatos. Agora vamos procurar todos os
operadores @ . Seja
@@ o operador base de
@ (pode ser o mesmo operador). Também encontramos todos os
operadores @@ e, para cada um deles, adicionamos sua versão invertida. De todos esses candidatos encontrados, selecionamos os mais adequados.
Observe que a sobrecarga do operador é permitida em
uma única passagem. Não estamos tentando substituir candidatos diferentes. Primeiro coletamos todos e depois escolhemos o melhor deles. Se isso não existir, a pesquisa, como antes, falha.
Agora, temos muito mais candidatos em potencial e, portanto, mais incerteza. Considere o seguinte exemplo:
struct C { bool operator==(C const&) const; bool operator!=(C const&) const; }; bool check(C x, C y) { return x != y; }
No C ++ 17, tínhamos apenas um candidato para
x! = Y e agora existem três:
x.operator! = (Y) ,! X.operator == (y) e
! Y.operator == (x) . O que escolher? Eles são todos iguais! (Nota: o candidato
y.operator! = (X) não existe, pois apenas operadores básicos podem ser
invertidos .)
Duas regras adicionais foram introduzidas para remover essa incerteza. Candidatos não convertidos são preferíveis a convertidos; . ,
x.operator!=(y) «»
!x.operator==(y) , «»
!y.operator==(x) . , «» .
:
operator@@ . . , .
-. — (,
x < y , —
(x <=> y) < 0 ), (,
x <=> y void - , DSL), . . ,
bool ( :
operator== bool , ?)
Por exemplo:
struct Base { friend bool operator<(const Base&, const Base&);
d1 < d2 :
#1 #2 . —
#2 , , , . ,
d1 < d2 (d1 <=> d2) < 0 . ,
void 0 — , . , - ,
#1 .
, , C++17, . , - . :
, . .
. , , , , , ( ). , :
« » , , ..
a < b 0 < (b <=> a) , , , .
C++17 . . :
struct A { T t; U u; V v; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u && v == rhs.v; } bool operator!=(A const& rhs) const { return !(*this == rhs); } bool operator< (A const& rhs) const {
-
std::tie() , .
, : :
struct A { T t; U u; V v; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u && v == rhs.v; } strong_ordering operator<=>(A const& rhs) const {
.
<=> < . , .
c != 0 , , (
), .
. C++20 , :
struct A { T t; U u; V v; bool operator==(A const& rhs) const = default; strong_ordering operator<=>(A const& rhs) const = default; };
, . , :
struct A { T t; U u; V v; bool operator==(A const& rhs) const = default; auto operator<=>(A const& rhs) const = default; };
. , , :
struct A { T t; U u; V v; auto operator<=>(A const& rhs) const = default; };
, , . :
operator== ,
operator<=> .
C++20: . . , , , .
PVS-Studio , <=> . , -. , , (. "
"). ++ .
PVS-Studio <, :
bool operator< (A const& rhs) const { return t < rhs.t && u < rhs.u; }
. , - . .
:
Comparisons in C++20 .