<=>
. Há algum tempo, postamos por nosso próprio Simon Brand detalhando algumas informações sobre esse novo operador, além de algumas informações conceituais sobre o que é e o que faz. O objetivo deste post é explorar algumas aplicações concretas desse novo operador estranho e de sua contraparte associada, o operator==
(sim, foi alterado, para melhor!), Ao mesmo tempo em que fornece algumas diretrizes para seu uso no código cotidiano.
Comparações
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: os leitores de olhos de águia perceberão que isso é realmente menos detalhado do que deveria ser no código pré-C ++ 20, porque essas funções devem ser todas amigas de não-membros, mais sobre isso mais tarde.
Isso é muito código clichê para escrever apenas para garantir que meu tipo seja comparável a algo do mesmo tipo. Bem, OK, lidamos com isso por um tempo. Então vem alguém que escreve isso:
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 este programa não será compilado.
error C3615: constexpr function 'is_lt' cannot result in a constant expression
Ah! O problema é que esquecemos o
constexpr
em nossa função de comparação, drat! Então, adicionamos constexpr
a todos os operadores de comparação. Alguns dias depois, alguém adiciona um ajudante is_gt
mas percebe que todos os operadores de comparação não têm uma especificação de exceção e passa pelo mesmo processo tedioso de adicionar noexcept
a cada uma das 5 sobrecargas.É aqui que o novo operador de espaçonave do C ++ 20 entra em cena para nos ajudar. Vamos ver como o
IntWrapper
original pode ser escrito em um 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ários para o operador da nave espacial retornar um tipo apropriado para nossa função padrão. No snippet acima, o tipo de retorno auto
será deduzido para std::strong_ordering
.Não apenas removemos 5 linhas supérfluas, mas nem precisamos definir nada, o compilador faz isso por nós! Nosso
is_lt
permanece inalterado e apenas funciona enquanto ainda está sendo constexpr
, embora não tenhamos explicitamente especificado isso em nosso operator<=>
padrão operator<=>
. Isso é bom e bom, mas algumas pessoas podem estar coçando a cabeça por que o is_lt
ainda pode compilar, mesmo que nem sequer use o operador da nave espacial. Vamos explorar a resposta a esta pergunta.Reescrevendo expressões
No C ++ 20, o compilador é apresentado a um novo conceito chamado de expressões "reescritas". O operador da nave espacial, juntamente com o
operator==
, estão entre os dois primeiros candidatos sujeitos a expressões reescritas. Para um exemplo mais concreto de reescrita de expressão, vamos is_lt
o exemplo fornecido em is_lt
.Durante a resolução de sobrecarga, o compilador selecionará dentre um conjunto de candidatos viáveis, que correspondem ao operador que estamos procurando. O processo de coleta de candidatos é alterado levemente para o caso de operações relacionais e de equivalência em que o compilador também deve reunir candidatos especiais reescritos e sintetizados ( [over.match.oper] /3.4 ).
Para nossa expressão
a < b
o padrão declara que podemos procurar o tipo de a para um operator<=>
ou um operator<=>
função de escopo de espaço de nome operator<=>
que aceite seu tipo. Portanto, o compilador faz e descobre que, de fato, o tipo de a IntWrapper::operator<=>
. O compilador pode então 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 se perguntar por que essa expressão reescrita é válida e correta. A correção da expressão na verdade decorre da semântica que o operador da nave espacial fornece. A
<=>
é uma comparação de três vias que implica que você obtém não apenas um resultado binário, mas uma ordem (na maioria dos casos) e, se você tiver uma ordem, poderá expressá-la em termos de quaisquer operações relacionais. Um exemplo rápido, a expressão 4 <=> 5 em C ++ 20 retornará o resultado std::strong_ordering::less
. O resultado std::strong_ordering::less
implica que 4
não é apenas diferente de 5
mas é estritamente menor que esse valor, isso torna a aplicação da operação (4 <=> 5) < 0
correta e exata para descrever nosso resultado.Usando as informações acima, o compilador pode pegar qualquer operador relacional generalizado (por exemplo,
<
, >
, etc.) e reescrevê-lo em termos do operador da nave espacial. No padrão, a expressão reescrita é frequentemente referida como (a <=> b) @ 0
onde @
representa qualquer operação relacional.Sintetizando expressões
Os leitores podem ter notado a menção sutil das expressões "sintetizadas" acima e também desempenham um papel nesse processo de reescrita do operador. Considere uma função de predicado diferente:
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 em terrenos pré-C ++ 20, e a maneira de resolver esse problema seria adicionar algumas funções de
friend
extras ao IntWrapper
que ficam do lado esquerdo do int
. Se você tentar criar esse exemplo com um compilador C ++ 20 e nossa definição de IntWrapper
C ++ 20, poderá notar que, novamente, “simplesmente funciona” - outro arranhador de cabeça. Vamos examinar por que o código acima ainda pode compilar no C ++ 20.Durante a resolução de sobrecarga, o compilador também reunirá o que o padrão se refere como candidatos "sintetizados" ou uma expressão reescrita com a ordem dos parâmetros revertidos. 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 evoca a expressão "sintetizada" 0 < (a <=> 42)
e descobre que há uma conversão de int
para IntWrapper
por meio de seu construtor de conversão, para que esse candidato seja usado.O objetivo das expressões sintetizadas é evitar a bagunça de precisar escrever o clichê das funções 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 únicos de classes; ele gera um conjunto correto de comparações para todos os subobjetos de 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 membros de classes que são matrizes em suas listas de subobjetos e compará-los recursivamente. Obviamente, se você quiser escrever os corpos dessas funções, ainda terá o benefício das expressões de reescrita do compilador para você.
Parece um pato, nada como um pato e Quacks como operator==
Algumas pessoas muito inteligentes do comitê de padronização perceberam que o operador da nave espacial sempre fará uma comparação lexicográfica de elementos, não importa o quê. Executar comparações lexicográficas incondicionalmente pode levar a um código gerado ineficiente com o operador de igualdade em particular.
O exemplo canônico está comparando duas strings. Se você tiver a string
"foobar"
e compará-la com a string "foo"
usando ==, seria de esperar que a operação fosse quase constante. O algoritmo eficiente de comparação de strings é assim:- Primeiro compare o tamanho das duas strings, se os tamanhos diferirem retornam
false
, caso contrário - percorra cada elemento das duas seqüências em uníssono e compare até que uma seja diferente ou que o fim seja alcançado, retorne o resultado.
Sob as regras do operador de espaçonave, precisamos começar pela comparação profunda de cada elemento primeiro até encontrarmos o que é diferente. No nosso exemplo de
"foobar"
e "foo"
somente ao comparar 'b'
a '\0'
, você finalmente retorna false
.Para combater isso, havia um documento, P1185R2 , que detalha uma maneira de o compilador reescrever e gerar 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; };
Apenas mais um passo ... no entanto, há boas notícias; você realmente não precisa escrever o código acima, porque simplesmente escrever o
auto operator<=>(const IntWrapper&) const = default
é suficiente para que o compilador gere implicitamente o operator==
separado - e mais eficiente - para você!O compilador aplica uma regra de "reescrita" ligeiramente alterada, específica para
==
e !=
Onde esses operadores são reescritos em termos de operator==
e não operator<=>
. Isso significa que !=
Também se beneficia da otimização.O código antigo não quebra
Nesse ponto, você deve estar pensando: OK, se o compilador tiver permissão para executar esse negócio de reescrita do operador, o que acontece quando tento ser mais esperto que 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 aqui é que você não fez. O modelo de resolução de sobrecarga em C ++ tem essa arena em que todos os candidatos batalham, e nessa batalha específica, temos 3 candidatos:
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 aceitássemos as regras de resolução de sobrecarga no C ++ 17, o resultado dessa chamada teria sido ambíguo, mas as regras de resolução de sobrecarga do C ++ 20 foram alteradas para permitir que o compilador resolva essa situação com a sobrecarga mais lógica.
Há uma fase de resolução de sobrecarga em que o compilador deve executar uma série de desempatadores. No C ++ 20, existe um novo desempatador que afirma que devemos preferir sobrecargas que não são reescritas ou sintetizadas, isso torna nosso sobrecarga
IntWrapper::operator<
o melhor candidato e resolve a ambiguidade. Esse mesmo mecanismo impede que os candidatos sintetizados pisem em expressões regulares reescritas.Pensamentos finais
O operador de nave espacial é uma adição bem-vinda ao C ++ e é um dos recursos que simplificará e ajudará você a escrever menos código e, às vezes, menos é mais. Então aperte o cinto com o operador de nave espacial C ++ 20!
Pedimos que você experimente o operador da nave espacial, que está disponível no Visual Studio 2019 em
/std:c++latest
! Como uma observação, as alterações introduzidas pelo 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é que o C ++ 20 seja finalizado.Como sempre, agradecemos seus comentários. Sinta-se à vontade para enviar qualquer comentário por email para visualcpp@microsoft.com , pelo Twitter @visualc ou pelo Facebook no Microsoft Visual Cpp . Além disso, sinta-se à vontade para me seguir no Twitter @starfreakclone .
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, informe-nos pelo DevComm.