<=> . 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 expressionO 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.