Olá novamente. Estamos compartilhando com você um artigo interessante, cuja tradução foi preparada especificamente para os alunos do curso "Desenvolvedor C ++" .
Hoje temos uma postagem de convidado de dám Balázs. Adam é engenheiro de software da Verizon Smart Communities Hungary e desenvolve análise de vídeo para sistemas embarcados. Uma de suas paixões é a otimização do tempo de compilação, então ele imediatamente concordou em escrever um post sobre este tópico. Você pode encontrar Adam online no
LinkedIn .
Em uma
série de artigos sobre como tornar o SFINAE elegante , vimos como tornar nosso modelo SFINAE bastante
conciso e expressivo .
Basta dar uma olhada em sua forma original:
template<typename T> class MyClass { public: void MyClass(T const& x){} template<typename T_ = T> void f(T&& x, typename std::enable_if<!std::is_reference<T_>::value, std::nullptr_t>::type = nullptr){} };
E compare-o com esta forma mais expressiva:
template<typename T> using IsNotReference = std::enable_if_t<!std::is_reference_v<T>>; template<typename T> class MyClass { public: void f(T const& x){} template<typename T_ = T, typename = IsNotReference <T_>> void f(T&& x){} };
Podemos razoavelmente acreditar que já é possível relaxar e começar a usá-lo na produção. Poderíamos, funciona na maioria dos casos, mas - como falamos sobre interfaces - nosso código deve ser seguro e confiável. É isso mesmo? Vamos tentar hackear!
Falha # 1: SFINAE pode ser contornado
Normalmente, o SFINAE é usado para desativar parte do código, dependendo da condição. Isso pode ser muito útil se precisarmos implementar, por exemplo, a função definida pelo usuário abs por qualquer motivo (classe aritmética definida pelo usuário, otimização para um equipamento específico, para fins de treinamento etc.):
template< typename T > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } int main() { int a{ std::numeric_limits< int >::max() }; std::cout << "a: " << a << " myAbs( a ): " << myAbs( a ) << std::endl; }
Este programa exibe o seguinte, que parece bastante normal:
a: 2147483647 myAbs( a ): 2147483647
Mas podemos chamar nossa função
abs
com argumentos não assinados
T
, e o efeito será catastrófico:
nt main() { unsigned int a{ std::numeric_limits< unsigned int >::max() }; std::cout << "a: " << a << " myAbs( a ): " << myAbs( a ) << std::endl; }
De fato, agora o programa exibe:
a: 4294967295 myAbs( a ): 1
Nossa função não foi projetada para trabalhar com argumentos não assinados, portanto, devemos limitar o conjunto possível de
T
com SFINAE:
template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T > >; template< typename T, typename = IsSigned< T > > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); }
O código funciona conforme o esperado: uma chamada para myAbs com um tipo não assinado causa um erro em tempo de compilação:
candidate template ignored: requirement 'std::is_signed_v<
unsigned int>' was not satisfied [with T = unsigned int]
Hacking SFINAE State
Então, o que há de errado com essa função? Para responder a essa pergunta, precisamos verificar como o myAbs implementa o SFINAE.
template< typename T, typename = IsSigned<T> > T myAbs( T val );
myAbs
é um modelo de função com dois tipos de parâmetros de modelo de entrada. O primeiro é o tipo real do argumento da função, o segundo é o
IsSigned <
T >
padrão (caso contrário,
std::enable_if_t <
std::is_signed_v <
T >
>
ou então
std::enable_if <
std::is_signed_v <
T>, void>::type
, que é
void
ou falhou na substituição).
Como podemos chamar
myAbs
? Existem 3 maneiras:
int a{ myAbs( -5 ) }; int b{ myAbs< int >( -5 ) }; int c{ myAbs< int, void >( -5 ) };
A primeira e a segunda chamadas são diretas, mas a terceira é interessante: qual é o argumento do modelo de
void
?
O segundo parâmetro do modelo é anônimo, tem um tipo padrão, mas ainda é um parâmetro do modelo, para que você possa especificá-lo explicitamente. Isso é um problema? Nesse caso, esse é realmente um grande problema. Podemos usar o terceiro formulário para contornar nossa verificação do SFINAE:
unsigned int d{ myAbs< unsigned int, void >( 5u ) }; unsigned int e{ myAbs< unsigned int, void >( std::numeric_limits< unsigned int >::max() ) };
Esse código compila bem, mas leva a resultados desastrosos, para evitar o que usamos SFINAE:
a: 4294967295 myAbs( a ): 1
Vamos resolver esse problema - mas primeiro: existem outras desvantagens? Bem ...
Falha # 2: não podemos ter implementações específicas
Outro uso comum do SFINAE é fornecer implementações específicas para condições específicas de tempo de compilação. E se não quisermos banir completamente a chamada de
myAbs
com valores
myAbs
e fornecer uma implementação trivial para esses casos? Podemos usar if constexpr no C ++ 17 (discutiremos isso mais tarde) ou podemos:
template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T > >; template< typename T > using IsUnsigned = std::enable_if_t< std::is_unsigned_v< T > >; template< typename T, typename = IsSigned< T > > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } template< typename T, typename = IsUnsigned< T > > T myAbs( T val ) { return val; }
Mas o que é isso?
error: template parameter redefines default argument template< typename T, typename = IsUnsigned< T > > note: previous default template argument defined here template< typename T, typename = IsSigned< T > >
Ah, o padrão C ++ (C ++ 17; §17.1.16) declara o seguinte :"Os argumentos padrão não devem ser fornecidos ao parâmetro do modelo por duas declarações diferentes no mesmo escopo."
Opa, foi exatamente isso que fizemos ...
Por que não usar regularmente se?
Poderíamos apenas usar se em tempo de execução:
template< typename T > T myAbs( T val ) { if( std::is_signed_v< T > ) { return ( ( val <= -1 ) ? -val : val ); } else { return val; } }
O compilador otimizaria a condição porque
if (std::is_signed_v <
T>)
se
if (std::is_signed_v < T>)
if (true)
ou
if (false)
após a criação do modelo. Sim, com nossa implementação atual de
myAbs
isso funcionará. Mas, no geral, isso impõe uma limitação enorme: as
else
if
e
else
devem ser válidas para cada
T
E se mudarmos um pouco a nossa implementação:
template< typename T > T myAbs( T val ) { if( std::is_signed_v< T > ) { return std::abs( val ); } else { return val; } } int main() { unsigned int a{ myAbs( 5u ) }; }
Nosso código falhará imediatamente:
error: call of overloaded 'abs(unsigned int&)' is ambiguous
Essa restrição é o que o SFINAE elimina: podemos escrever um código válido apenas para um subconjunto de T (em myAbs é válido apenas para tipos não assinados ou válido apenas para tipos assinados).
Solução: outro formulário para SFINAE
O que podemos fazer para superar essas deficiências? Para o primeiro problema, devemos forçar nossa verificação SFINAE, independentemente de como os usuários invocam nossa função. Atualmente, nosso teste pode ser contornado quando o compilador não precisa do tipo padrão para o segundo parâmetro do modelo.
E se usarmos nosso código SFINAE para declarar um tipo de parâmetro de modelo em vez de fornecer um tipo padrão? Vamos tentar:
template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >; template< typename T, IsSigned< T > = true > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } int main() {
Precisamos que IsSigned seja um tipo diferente de nulo em casos válidos, porque queremos fornecer um valor padrão para esse tipo. Não há valor para o tipo void; portanto, devemos usar outra coisa: bool, int, enum, nullptr_t, etc. Normalmente, eu uso bool - nesse caso, as expressões parecem significativas:
template< typename T, IsSigned< T > = true >
Isso funciona! Para
myAbs (5u)
compilador gera um erro, como antes:
candidate template ignored: requirement 'std::is_signed_v<unsigned int>' was not satisfied [with T = unsigned int
A segunda chamada,
myAbs <
int> (5u)
ainda é válida, dizemos explicitamente ao tipo
T
do compilador, portanto ele converte
5u
em
int
.
Finalmente, não podemos mais rastrear
myAbs
no dedo:
myAbs <
unsigned int, true> (5u)
gera um erro. Não importa se fornecemos ou não um valor padrão na chamada, parte da expressão SFINAE é avaliada de qualquer maneira, porque o compilador precisa de um tipo de argumento com um valor de modelo anônimo.
Podemos avançar para o próximo problema - mas espere um minuto! Acho que não substituímos o argumento padrão do mesmo parâmetro de modelo.Qual era a situação original?
template< typename T, typename = IsUnsigned< T > > T myAbs( T val ); template< typename T, typename = IsSigned< T > > T myAbs( T val );
Mas agora com o código atual:
template< typename T, IsUnsigned< T > = true > T myAbs( T val ); template< typename T, IsSigned< T > = true > T myAbs( T val );
Parece muito semelhante ao código anterior, portanto, podemos pensar que isso também não funcionará, mas, na verdade, esse código não tem o mesmo problema. O que é
IsUnsigned <
T>
? Pesquisa booliana ou com falha. E o que é
IsSigned <
T>
? A mesma coisa, mas se um deles é Bool, o outro é uma falha na pesquisa.
Isso significa que não substituímos os argumentos padrão, uma vez que existe apenas uma função com o argumento bool do modelo, a outra é uma substituição com falha e, portanto, ela não existe.
Açúcar sintático
UPD Este parágrafo foi excluído pelo autor devido a erros encontrados nele.Versões antigas do C ++
Todas as opções acima funcionam com o C ++ 11, a única diferença é a verbosidade das definições de restrições entre as versões padrão:
Mas o modelo permanece o mesmo:
template< typename T, IsSigned< T > = true >
No bom e velho C ++ 98, não há aliases de modelo; além disso, os modelos de função não podem ter tipos ou valores padrão. Podemos inserir nosso código SFINAE no tipo de resultado ou apenas na lista de parâmetros de função. A segunda opção é recomendada porque os construtores não têm tipos de resultado. O melhor que podemos fazer é algo assim:
template< typename T > T myAbs( T val, typename my_enable_if< my_is_signed< T >::value, bool >::type = true ) { return( ( val <= -1 ) ? -val : val ); }
Apenas para comparação - a versão moderna do C ++:
template< typename T, IsSigned< T > = true > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); }
A versão do C ++ 98 é feia, apresenta um parâmetro sem sentido, mas funciona - você pode usá-lo se for absolutamente necessário. E sim:
my_enable_if
e
my_is_signed
devem ser implementados (
std :: enable_if std :: is_signed
eram novos no C ++ 11).
Estado atual
O C ++ 17 introduziu
if constexpr
, um método para descartar códigos com base em condições em tempo de compilação. As instruções if e else devem estar sintaticamente corretas, mas a condição será avaliada em tempo de compilação.
template< typename T > T myAbs( T val ) { if constexpr( std::is_signed_v< T > ) { return( ( val <= -1 ) ? -val : val ); } else { if constexpr( std::is_unsigned_v< T > ) { return val; } } }
Como podemos ver, nossa função abs tornou-se mais compacta e fácil de ler. No entanto, o manuseio de tipos não conformes não é simples. O
static_assert
e incondicional torna essa afirmação pouco consistente, o que é proibido pelo padrão, independentemente de ser descartado ou não.
Felizmente, existe uma brecha: nos objetos de modelo, os operadores descartados não são criados se a condição for independente do valor. Ótimo!
Portanto, o único problema com o nosso código é que ele falha durante a definição do modelo. Se pudéssemos adiar a avaliação de
static_assert
até o momento em que o modelo foi criado, o problema seria resolvido: ele seria criado se e somente se todas as nossas condições forem falsas. Mas como podemos adiar
static_assert
até que o modelo seja criado? Torne sua condição dependente do tipo!
template< typename > inline constexpr bool dependent_false_v{ false }; template< typename T > T myAbs( T val ) { if constexpr( std::is_signed_v< T > ) { return( ( val <= -1 ) ? -val : val ); } else { if constexpr( std::is_unsigned_v< T > ) { return val; } else { static_assert( dependent_false_v< T >, "Unsupported type" ); } } }
Sobre o futuro
Já estamos realmente próximos, mas precisamos esperar um pouco até o C ++ 20 trazer a solução final: conceitos! Isso mudará completamente a maneira como os modelos (e SFINAE) são usados.
Em poucas palavras: conceitos podem ser usados para limitar o conjunto de argumentos que são aceitos para os parâmetros do modelo. Para nossa função abs, poderíamos usar o seguinte conceito:
template< typename T > concept bool Arithmetic() { return std::is_arithmetic_v< T >; }
E como podemos usar conceitos? Existem três maneiras:
Observe que o terceiro formulário ainda declara uma função de modelo! Aqui está a implementação completa de myAbs em C ++ 20:
template< typename T > concept bool Arithmetic() { return std::is_arithmetic_v< T >; } Arithmetic myAbs( Arithmetic val ) { if constexpr( std::is_signed_v< decltype( val ) > ) { return( ( val <= -1 ) ? -val : val ); } else { return val; } } int main() { unsigned int a{ myAbs( 5u ) }; int b{ myAbs< int >( 5u ) };
Uma chamada comentada fornece o seguinte erro:
error: cannot call function 'auto myAbs(auto:1) [with auto:1 = const char*]' constraints not satisfied within 'template<class T> concept bool Arithmetic() [with T = const char*]' concept bool Arithmetic(){ ^~~~~~~~~~ 'std::is_arithmetic_v' evaluated to false
Peço a todos que ousadamente usem esses métodos no código de produção; o tempo de compilação é mais barato que o tempo de execução. Feliz SFINAEing!