. Há algum tempo, postamos por nosso próprio Simon Brand ...">

Simplifique seu código com a Rocket Science: Operador da nave espacial C ++ 20

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

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


All Articles