. Há pouco tempo, Simon Brand publicou um post que continha informações conceituais d...">

O novo operador de nave espacial em C ++ 20

O C ++ 20 adiciona um novo operador chamado "nave espacial": <=> . 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.

Source: https://habr.com/ru/post/pt458242/


All Articles