<=>
. Há pouco tempo, Simon Brand publicou um post que continha informações conceituais detalhadas sobre o que é esse operador e para que fins ele é usado. A principal tarefa deste post é estudar as aplicações específicas do novo operador "estranho" e seu operator==
analógico operator==
, além de formular algumas recomendações para seu uso na codificação diária.
Comparação
Não é incomum ver código como o seguinte:
struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } bool operator==(const IntWrapper& rhs) const { return value == rhs.value; } bool operator!=(const IntWrapper& rhs) const { return !(*this == rhs); } bool operator<(const IntWrapper& rhs) const { return value < rhs.value; } bool operator<=(const IntWrapper& rhs) const { return !(rhs < *this); } bool operator>(const IntWrapper& rhs) const { return rhs < *this; } bool operator>=(const IntWrapper& rhs) const { return !(*this < rhs); } };
Nota: leitores atentos perceberão que isso é realmente menos detalhado do que deveria ser no código anterior ao C ++ 20. Mais sobre isso mais tarde.
Você precisa escrever muito código padrão para garantir que nosso tipo seja comparável a algo do mesmo tipo. Ok, vamos descobrir por um tempo. Então vem alguém que escreve assim:
constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) { return a < b; } int main() { static_assert(is_lt(0, 1)); }
A primeira coisa que você notará é que o programa não será compilado.
error C3615: constexpr function 'is_lt' cannot result in a constant expression
O problema é que o
constexpr
foi esquecido na função de comparação. Alguns adicionam constexpr
a todos os operadores de comparação. Alguns dias depois, alguém adicionará o is_gt
, mas observe que todos os operadores de comparação não possuem uma especificação de exceção, e você terá que passar pelo mesmo processo tedioso de adicionar noexcept
a cada uma das 5 sobrecargas.É aqui que o novo operador de espaçonave C ++ 20 vem em nosso auxílio. Vamos ver como você pode escrever o
IntWrapper
original no mundo C ++ 20: #include <compare> struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; };
A primeira diferença que você pode notar é a nova inclusão de
<compare>
. O cabeçalho <compare>
é responsável por preencher o compilador com todos os tipos de categorias de comparação necessárias para o operador da nave espacial, para que ele retorne um tipo adequado para nossa função padrão. No trecho acima, o tipo de retorno auto
será std::strong_ordering
.Não apenas excluímos 5 linhas extras, mas nem precisamos determinar nada, o compilador fará isso por nós.
is_lt
permanece inalterado e apenas funciona, enquanto constexpr
permanece, embora não tenhamos especificado isso explicitamente em nosso operator<=>
padrão operator<=>
. Isso é bom, mas algumas pessoas podem is_lt
por que o is_lt
pode compilar, mesmo que ele não use o operador da nave espacial. Vamos encontrar a resposta para esta pergunta.Reescrevendo Expressões
No C ++ 20, o compilador é introduzido em um novo conceito relacionado a expressões "reescritas". O operador da nave espacial, juntamente com o
operator==
, é um dos dois primeiros candidatos que podem ser reescritos. Para um exemplo mais específico de reescrever expressões, vejamos o exemplo dado em is_lt
.Ao resolver a sobrecarga, o compilador escolherá um conjunto dos candidatos mais adequados, cada um dos quais corresponde ao operador que precisamos. O processo de seleção muda levemente para operações de comparação e operações de equivalência, quando o compilador também deve coletar candidatos especiais transcritos e sintetizados ( [over.match.oper] /3.4 ).
Para nossa expressão
a < b
padrão declara que podemos procurar o tipo a
para funções de operator<=>
ou operator<=>
que aceitam esse tipo. É o que o compilador faz e descobre que o tipo a
realmente contém IntWrapper::operator<=>
. O compilador pode usar esse operador e reescrever a expressão a < b
como (a <=> b) < 0
. Essa expressão reescrita é usada como candidata à resolução normal de sobrecarga.Você pode perguntar por que essa expressão reescrita está correta. A correção da expressão realmente segue a semântica que o operador da nave espacial fornece.
<=>
é uma comparação de três vias, o que implica que você obtém não apenas um resultado binário, mas também um pedido (na maioria dos casos). Se você tiver um pedido, poderá expressá-lo em termos de qualquer operação de comparação. Um exemplo rápido, a expressão 4 <=> 5 em C ++ 20 retornará o resultado std::strong_ordering::less
. O resultado de std::strong_ordering::less
implica que 4
não 4
apenas diferente de 5
mas também estritamente menor que esse valor, o que torna a aplicação da operação (4 <=> 5) < 0
correta e precisa para descrever nosso resultado.Usando as informações acima, o compilador pode pegar qualquer operador de comparação generalizado (ou seja,
<
, >
, etc.) e reescrevê-lo em termos do operador da nave espacial. No padrão, uma expressão reescrita é frequentemente referida como (a <=> b) @ 0
onde @
representa qualquer operação de comparação.Sintetizando expressões
Os leitores podem ter notado uma referência sutil às expressões "sintetizadas" acima e também desempenham um papel nesse processo de reescrever instruções. Considere a seguinte função:
constexpr bool is_gt_42(const IntWrapper& a) { return 42 < a; }
Se usarmos nossa definição original para o
IntWrapper
, esse código não será compilado.error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)
Isso faz sentido antes do C ++ 20, e a maneira de resolver esse problema é adicionar algumas funções adicionais de
friend
ao IntWrapper
que ocupam o lado esquerdo do int
. Se você tentar criar este exemplo usando o compilador e a IntWrapper
C ++ 20, poderá notar que, novamente, ele simplesmente funciona. Vejamos por que o código acima ainda está compilando no C ++ 20.Ao resolver sobrecargas, o compilador também coletará o que o padrão chama de candidatos "sintetizados" ou uma expressão reescrita com a ordem inversa dos parâmetros. No exemplo acima, o compilador tentará usar a expressão reescrita
(42 <=> a) < 0
, mas descobrirá que não há conversão do IntWrapper
para int
para satisfazer o lado esquerdo, para que a expressão reescrita seja descartada. O compilador também chama a expressão "sintetizada" 0 < (a <=> 42)
e detecta que uma conversão de int
para IntWrapper
por meio de seu construtor de conversão, portanto esse candidato é usado.O objetivo das expressões sintetizadas é evitar a confusão de escrever modelos de função de
friend
para preencher as lacunas nas quais seu objeto pode ser convertido de outros tipos. Expressões sintetizadas são generalizadas para 0 @ (b <=> a)
.Tipos mais complexos
O operador de nave espacial gerado pelo compilador não para em membros individuais de classes; gera o conjunto correto de comparações para todos os subobjetos em seus tipos:
struct Basics { int i; char c; float f; double d; auto operator<=>(const Basics&) const = default; }; struct Arrays { int ai[1]; char ac[2]; float af[3]; double ad[2][2]; auto operator<=>(const Arrays&) const = default; }; struct Bases : Basics, Arrays { auto operator<=>(const Bases&) const = default; }; int main() { constexpr Bases a = { { 0, 'c', 1.f, 1. }, { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } }; constexpr Bases b = { { 0, 'c', 1.f, 1. }, { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } }; static_assert(a == b); static_assert(!(a != b)); static_assert(!(a < b)); static_assert(a <= b); static_assert(!(a > b)); static_assert(a >= b); }
O compilador sabe como expandir os membros da classe que são matrizes em suas listas de subobjetos e compará-los recursivamente. Obviamente, se você quiser escrever os corpos dessas funções, ainda se beneficiará da reescrita de expressões pelo compilador.
Parece um pato, nada como um pato e grasna como operator==
Algumas pessoas muito inteligentes do comitê de padronização notaram que o operador da nave espacial sempre fará uma comparação lexicográfica de elementos, não importa o quê. A execução incondicional de comparações lexicográficas pode levar a um código ineficiente, em particular, com o operador de igualdade.
Um exemplo canônico comparando duas linhas. Se você tiver a string
"foobar"
e compará-la com a string "foo"
usando ==, pode esperar que esta operação seja quase constante. Um algoritmo eficaz de comparação de strings é o seguinte:- Primeiro compare o tamanho das duas linhas. Se os tamanhos forem diferentes, retorne
false
- Caso contrário, percorra cada elemento de duas linhas passo a passo e compare-os até que haja uma diferença ou todos os elementos terminem. Retorne o resultado.
De acordo com as regras do operador de espaçonave, devemos começar comparando cada elemento até encontrar um que seja diferente. No nosso exemplo,
"foobar"
e "foo"
somente ao comparar 'b'
e '\0'
, você finalmente retorna false
.Para combater isso, havia o artigo P1185R2 , que detalha como o compilador reescreve e gera o
operator==
independentemente do operador da nave espacial. Nosso IntWrapper
pode ser escrito da seguinte maneira: #include <compare> struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; bool operator==(const IntWrapper&) const = default; };
Mais um passo ... no entanto, há boas notícias; você realmente não precisa escrever o código acima, porque basta escrever o
auto operator<=>(const IntWrapper&) const = default
suficiente para que o compilador gere implicitamente um operator==
separado e mais eficiente operator==
para você!O compilador aplica uma regra de "reescrita" levemente modificada, específica para
==
e !=
, Onde nesses operadores eles são reescritos em termos de operator==
vez de operator<=>
. Isso significa que !=
Também se beneficia da otimização.Código antigo não quebra
Neste ponto, você pode pensar: bem, se o compilador puder executar esta operação de reescrita do operador, o que acontecerá se eu tentar enganar o compilador:
struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; bool operator<(const IntWrapper& rhs) const { return value < rhs.value; } }; constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) { return a < b; }
A resposta não é grande coisa. O modelo de resolução de sobrecarga no C ++ é a arena na qual todos os candidatos se confrontam. Nesta batalha em particular, temos três deles:
IntWrapper::operator<(const IntWrapper& a, const IntWrapper& b)
IntWrapper::operator<=>(const IntWrapper& a, const IntWrapper& b)
(reescrito)
IntWrapper::operator<=>(const IntWrapper& b, const IntWrapper& a)
(sintetizado)
Se adotássemos regras de resolução de sobrecarga no C ++ 17, o resultado dessa chamada seria misto, mas as regras de resolução de sobrecarga do C ++ 20 foram alteradas para que o compilador pudesse resolver essa situação com a sobrecarga mais lógica.
Há uma fase de resolução de sobrecarga quando o compilador deve concluir uma série de passes extras. O C ++ 20 introduziu um novo mecanismo no qual as sobrecargas que não são substituídas ou sintetizadas são preferidas, o que faz com que nosso
IntWrapper::operator<
sobrecarregue o melhor candidato e resolva a ambiguidade. O mesmo mecanismo impede o uso de candidatos sintetizados em vez das expressões reescritas usuais.Considerações finais
O operador de nave espacial é uma adição bem-vinda ao C ++, pois pode ajudar a simplificar seu código e escrever menos e, às vezes, menos é melhor. Então aperte o cinto e controle sua nave espacial C ++ 20!
Pedimos que você experimente o operador de nave espacial, ele está disponível agora no Visual Studio 2019 em
/std:c++latest
! Como uma observação, as alterações feitas no P1185R2 estarão disponíveis no Visual Studio 2019 versão 16.2. Lembre-se de que o operador da nave espacial faz parte do C ++ 20 e está sujeito a algumas alterações até o momento em que o C ++ 20 é finalizado.Como sempre, aguardamos o seu feedback. Sinta-se à vontade para enviar qualquer comentário por e-mail para visualcpp@microsoft.com , via Twitter @visualc ou Facebook Microsoft Visual Cpp .
Se você encontrar outros problemas com o MSVC no VS 2019, informe-nos através da opção "Relatar um problema" , do instalador ou do próprio Visual Studio IDE. Para sugestões ou relatórios de erros, escreva-nos através do DevComm.