Abordagens para funcionar SFINAE em C ++

40

Estou usando fortemente a função SFINAE em um projeto e não tenho certeza se existem diferenças entre as duas abordagens a seguir (além do estilo):

#include <cstdlib>
#include <type_traits>
#include <iostream>

template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo()
{
    std::cout << "method 1" << std::endl;
}

template <class T, std::enable_if_t<std::is_same_v<T, double>>* = 0>
void foo()
{
    std::cout << "method 2" << std::endl;
}

int main()
{
    foo<int>();
    foo<double>();

    std::cout << "Done...";
    std::getchar();

    return EXIT_SUCCESS;
}

A saída do programa é conforme o esperado:

method 1
method 2
Done...

Vi o método 2 usado com mais frequência no stackoverflow, mas prefiro o método 1.

Existem circunstâncias em que essas duas abordagens diferem?

keith
fonte
Como você executa este programa? Não vai compilar para mim.
Igel Alter
@alter igel precisará de um compilador C ++ 17. Usei o MSVC 2019 para testar este exemplo, mas trabalho principalmente com o Clang.
24519 keith
Relacionados: why-deve-i-avoid-stdenable-se-in-função assinaturas e C ++ 20 introduz também novas maneiras com conceito :-)
Jarod42
@ Jarod42 Os conceitos são uma das coisas mais necessárias para mim do C ++ 20.
val diz Reintegrar Monica

Respostas:

35

Vi o método 2 usado com mais frequência no stackoverflow, mas prefiro o método 1.

Sugestão: prefira o método 2.

Ambos os métodos funcionam com funções únicas. O problema surge quando você possui mais de uma função, com a mesma assinatura, e deseja ativar apenas uma função do conjunto.

Suponha que você deseje ativar a foo()versão 1 quando bar<T>()(fingir que é uma constexprfunção) truee a foo()versão 2 quando bar<T>()for false.

Com

template <typename T, typename = std::enable_if_t<true == bar<T>()>>
void foo () // version 1
 { }

template <typename T, typename = std::enable_if_t<false == bar<T>()>>
void foo () // version 2
 { }

você recebe um erro de compilação porque possui uma ambiguidade: duas foo()funções com a mesma assinatura (um parâmetro de modelo padrão não altera a assinatura).

Mas a seguinte solução

template <typename T, std::enable_if_t<true == bar<T>(), bool> = true>
void foo () // version 1
 { }

template <typename T, std::enable_if_t<false == bar<T>(), bool> = true>
void foo () // version 2
 { }

funciona, porque SFINAE modifica a assinatura das funções.

Observação não relacionada: também existe um terceiro método: ativar / desativar o tipo de retorno (exceto para construtores de classe / estrutura, obviamente)

template <typename T>
std::enable_if_t<true == bar<T>()> foo () // version 1
 { }

template <typename T>
std::enable_if_t<false == bar<T>()> foo () // version 2
 { }

Como o método 2, o método 3 é compatível com a seleção de funções alternativas com a mesma assinatura.

max66
fonte
11
Obrigado pela grande explicação, vou preferir métodos 2 e 3 a partir de agora :-)
keith
"um parâmetro de modelo padrão não altera a assinatura" - como isso é diferente na sua segunda variante, que também usa parâmetros de modelo padrão?
Eric
11
@ Eric - Não é fácil dizer ... Suponho que a outra resposta explique isso melhor ... Se o SFINAE ativar / desativar o argumento padrão do modelo, a foo()função permanecerá disponível quando você o chamar com um segundo parâmetro explícito do modelo (a foo<double, double>();chamada). E se permanecer disponível, há uma ambiguidade com a outra versão. Com o método 2, SFINAE habilita / desabilita o segundo argumento, não o parâmetro padrão. Portanto, você não pode chamá-lo explicando o parâmetro porque há uma falha de substituição que não permite um segundo parâmetro. Assim, a versão não estiver disponível, então não há ambigüidade
max66
3
O método 3 tem a vantagem adicional de geralmente não vazar para o nome do símbolo. A variante auto foo() -> std::enable_if_t<...>geralmente é útil para evitar ocultar a assinatura da função e permitir o uso dos argumentos da função.
Deduplicator
@ max66: portanto, o ponto principal é que a falha na substituição em um parâmetro de modelo padrão não é um erro se o parâmetro for fornecido e nenhum padrão for necessário?
Eric
21

Além da resposta do max66 , outro motivo para preferir o método 2 é que, com o método 1, você pode (acidentalmente) passar um parâmetro de tipo explícito como o segundo argumento do modelo e derrotar completamente o mecanismo SFINAE. Isso pode ocorrer como um erro de digitação, copiar / colar ou como uma supervisão em um mecanismo de modelo maior.

#include <cstdlib>
#include <type_traits>
#include <iostream>

// NOTE: foo should only accept T=int
template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo(){
    std::cout << "method 1" << std::endl;
}

int main(){

    // works fine
    foo<int>();

    // ERROR: subsitution failure, as expected
    // foo<double>();

    // Oops! also works, even though T != int :(
    foo<double, double>();

    return 0;
}

Demonstração ao vivo aqui

alterar igel
fonte
Bom ponto. O mecanismo pode ser invadido.
max66