Por que devo evitar std :: enable_if nas assinaturas de função

165

Scott Meyers publicou o conteúdo e o status de seu próximo livro EC ++ 11. Ele escreveu que um item do livro poderia ser "Evitar std::enable_ifassinaturas de funções" .

std::enable_if pode ser usado como argumento de função, como tipo de retorno ou como modelo de classe ou parâmetro de modelo de função para remover condicionalmente funções ou classes da resolução de sobrecarga.

Em esta questão todos os três solução são mostrados.

Como parâmetro de função:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

Como parâmetro do modelo:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

Como tipo de retorno:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • Qual solução deve ser preferida e por que devo evitar outras?
  • Em que casos "Evitar std::enable_ifassinaturas de função" refere-se ao uso como tipo de retorno (que não faz parte da assinatura normal da função, mas de especializações de modelo)?
  • Existem diferenças para os modelos de função membro e não membro?
hansmaad
fonte
Porque sobrecarregar é tão bom, geralmente. Se houver, delegar para uma implementação que usa modelos de classe (especializados).
sehe
As funções de membro diferem no fato de o conjunto de sobrecargas incluir sobrecargas declaradas após a sobrecarga atual. Isso é particularmente importante ao fazer o tipo de retorno atrasado variadics (em que o tipo de retorno deve ser deduzido de outra sobrecarga)
consulte
1
Bem, apenas subjetivamente, devo dizer que, embora muitas vezes seja bastante útil, não gosto std::enable_ifde bagunçar minhas assinaturas de funções (especialmente a nullptrversão feia de argumentos de funções adicionais ) porque sempre parece com o que é, um truque estranho (para algo que static ifpode faça muito mais bonito e limpo) usando o template black-magic para explorar um recurso de linguagem interessante. É por isso que eu prefiro o envio de tags sempre que possível (bem, você ainda tem argumentos estranhos adicionais, mas não na interface pública e também muito menos feia e enigmática ).
Christian Rau
2
Eu quero perguntar o que faz =0em typename std::enable_if<std::is_same<U, int>::value, int>::type = 0realizar? Não consegui encontrar os recursos corretos para entendê-lo. Eu sei que a primeira parte anterior =0tem um tipo de membro intse Ue inté o mesmo. Muito Obrigado!
Astroboylrx
4
@astroboylrx Engraçado, eu ia colocar um comentário observando isso. Basicamente, esse = 0 indica que este é um parâmetro de modelo não padrão do tipo . Isso é feito dessa maneira porque os parâmetros padrão do modelo de tipo não fazem parte da assinatura; portanto, você não pode sobrecarregá-los.
Nir Friedman

Respostas:

107

Coloque o hack nos parâmetros do modelo .

A enable_ifabordagem do parâmetro on template possui pelo menos duas vantagens sobre as outras:

  • legibilidade : o uso enable_if e os tipos de retorno / argumento não são mesclados em um pedaço confuso de desambiguadores de nomes de tipos e acessos de tipos aninhados; mesmo que a desordem do tipo desambiguador e aninhado possa ser atenuada com modelos de alias, isso ainda mesclaria duas coisas não relacionadas. O uso enable_if está relacionado aos parâmetros do modelo e não aos tipos de retorno. Tê-los nos parâmetros do modelo significa que eles estão mais próximos do que importa;

  • aplicabilidade universal : os construtores não têm tipos de retorno e alguns operadores não podem ter argumentos extras; portanto, nenhuma das outras duas opções pode ser aplicada em qualquer lugar. Colocar enable_if em um parâmetro de modelo funciona em qualquer lugar, pois você só pode usar SFINAE em modelos de qualquer maneira.

Para mim, o aspecto da legibilidade é o grande fator motivador dessa escolha.

R. Martinho Fernandes
fonte
4
O uso da FUNCTION_REQUIRESmacro aqui torna muito mais agradável a leitura e funciona também nos compiladores C ++ 03, e depende do uso enable_ifno tipo de retorno. Além disso, o uso de enable_ifparâmetros no modelo de função causa problemas de sobrecarga, porque agora a assinatura da função não é exclusiva, causando erros ambíguos de sobrecarga.
Paul Fultz II
3
Esta é uma pergunta antiga, mas para quem ainda está lendo: a solução para o problema levantado pelo @Paul é usar enable_ifum parâmetro de modelo não-tipo padrão, que permite sobrecarga. Ou seja, em enable_if_t<condition, int> = 0vez de typename = enable_if_t<condition>.
Nir Friedman
link do wayback para quase-static-if: web.archive.org/web/20150726012736/http://flamingdangerzone.com/…
davidbak
@ R.MartinhoFernandes O flamingdangerzonelink no seu comentário parece levar a uma página de instalação de spyware agora. Eu o sinalizei para atenção do moderador.
nispio 13/05
58

std::enable_ifconfia no princípio " Falha na substituição não é um erro " (também conhecido como SFINAE) durante a dedução do argumento do modelo . Esse é um recurso de linguagem muito frágil e você precisa ter muito cuidado para corrigi-lo.

  1. se sua condição enable_ifcontiver um modelo aninhado ou uma definição de tipo (dica: procure ::tokens), a resolução desses tempatles ou tipos aninhados geralmente será um contexto não deduzido . Qualquer falha de substituição em um contexto não deduzido é um erro .
  2. as várias condições em várias enable_ifsobrecargas não podem ter nenhuma sobreposição porque a resolução da sobrecarga seria ambígua. Isso é algo que você, como autor, precisa verificar por si mesmo, apesar de receber bons avisos do compilador.
  3. enable_ifmanipula o conjunto de funções viáveis ​​durante a resolução de sobrecarga, o que pode ter interações surpreendentes, dependendo da presença de outras funções trazidas de outros escopos (por exemplo, através de ADL). Isso o torna não muito robusto.

Em resumo, quando funciona, funciona, mas quando não funciona, pode ser muito difícil depurar. Uma alternativa muito boa é usar o envio de tags , ou seja, delegar para uma função de implementação (geralmente em um detailespaço para nome ou em uma classe auxiliar) que recebe um argumento fictício com base na mesma condição de tempo de compilação usada na enable_if.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

O envio de tags não manipula o conjunto de sobrecargas, mas ajuda a selecionar exatamente a função desejada, fornecendo os argumentos adequados por meio de uma expressão em tempo de compilação (por exemplo, em uma característica de tipo). Na minha experiência, isso é muito mais fácil de depurar e acertar. Se você é um aspirante a escritor de bibliotecas de características sofisticadas, pode precisar de enable_ifalguma forma, mas para o uso mais regular de condições de tempo de compilação não é recomendado.

TemplateRex
fonte
22
O envio de tags tem uma desvantagem: se você tem alguma característica que detecta a presença de uma função e essa função é implementada com a abordagem de envio de tags, sempre informa esse membro como presente e resulta em um erro em vez de uma falha potencial de substituição . O SFINAE é principalmente uma técnica para remover sobrecargas dos conjuntos candidatos, e o envio de tags é uma técnica para selecionar entre duas (ou mais) sobrecargas. Há alguma sobreposição na funcionalidade, mas elas não são equivalentes.
R. Martinho Fernandes
@ R.MartinhoFernandes, você pode dar um pequeno exemplo e ilustrar como isso enable_iffaria certo?
TemplateRex
1
@ R.MartinhoFernandes Acho que uma resposta separada que explica esses pontos pode agregar valor ao OP. :-) Aliás, escrever características como is_f_ableé algo que considero uma tarefa para os escritores de bibliotecas que, obviamente, podem usar o SFINAE quando isso lhes dá uma vantagem, mas para usuários "regulares" e com uma característica is_f_able, acho que o envio de tags é mais fácil.
TemplateRex
1
@hansmaad Publiquei uma resposta curta abordando sua pergunta e abordarei a questão de "para SFINAE ou não SFINAE" em uma postagem de blog (isso é um pouco estranho nesta questão). Assim que eu tiver tempo de terminar, quero dizer.
R. Martinho Fernandes
8
SFINAE é "frágil"? O que?
Lightness Races em órbita em
5

Qual solução deve ser preferida e por que devo evitar outras?

  • O parâmetro do modelo

    • É utilizável em construtores.
    • É utilizável no operador de conversão definido pelo usuário.
    • Requer C ++ 11 ou posterior.
    • É IMO, mais legível.
    • Pode ser facilmente utilizado incorretamente e produz erros com sobrecargas:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()

    Observe em typename = std::enable_if_t<cond>vez de corrigirstd::enable_if_t<cond, int>::type = 0

  • tipo de retorno:

    • Não pode ser usado no construtor. (sem tipo de retorno)
    • Ele não pode ser usado no operador de conversão definido pelo usuário. (não dedutível)
    • Pode ser usado antes do C ++ 11.
    • Segundo IMO mais legível.
  • Por último, no parâmetro de função:

    • Pode ser usado antes do C ++ 11.
    • É utilizável em construtores.
    • Ele não pode ser usado no operador de conversão definido pelo usuário. (sem parâmetros)
    • Ele não pode ser utilizado em métodos com número fixo de argumentos (operadores unárias / binários +, -,* , ...)
    • Pode ser usado com segurança na herança (veja abaixo).
    • Alterar assinatura da função (você tem basicamente um extra como último argumento void* = nullptr) (para que o ponteiro da função seja diferente e assim por diante)

Existem diferenças para os modelos de função membro e não membro?

Existem diferenças sutis com herança e using :

De acordo com using-declarator (ênfase minha):

namespace.udecl

O conjunto de declarações introduzidas pelo using-declarator é encontrado executando uma pesquisa de nome qualificado ([basic.lookup.qual], [class.member.lookup]) para o nome no using-declarator, excluindo funções ocultas conforme descrito abaixo.

...

Quando um declarador de uso traz declarações de uma classe base para uma classe derivada, as funções de membro e os modelos de função de membro na classe derivada substituem e / ou ocultam as funções de membro e os modelos de função de membro com o mesmo nome, lista de tipo de parâmetro, cv- qualificação e ref-qualifier (se houver) em uma classe base (em vez de em conflito). Tais declarações ocultas ou substituídas são excluídas do conjunto de declarações introduzidas pelo declarador using.

Portanto, para o argumento do modelo e o tipo de retorno, os métodos estão ocultos no seguinte cenário:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

Demo (o gcc encontra incorretamente a função base).

Considerando que com argumento, cenário semelhante funciona:

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

Demo

Jarod42
fonte