Exemplos C ++ SFINAE?

122

Quero entrar em mais meta-programação de modelos. Eu sei que SFINAE significa "falha na substituição não é um erro". Mas alguém pode me mostrar um bom uso da SFINAE?

rlbond
fonte
2
Essa é uma boa pergunta. Eu entendo o SFINAE muito bem, mas acho que nunca precisei usá-lo (a menos que as bibliotecas o façam sem que eu saiba).
Zifre
5
O STL coloca de maneira um pouco diferente nas perguntas frequentes aqui , "Falha na substituição não é um elefante"
vulcan raven

Respostas:

72

Aqui está um exemplo ( daqui ):

template<typename T>
class IsClassT {
  private:
    typedef char One;
    typedef struct { char a[2]; } Two;
    template<typename C> static One test(int C::*);
    // Will be chosen if T is anything except a class.
    template<typename C> static Two test(...);
  public:
    enum { Yes = sizeof(IsClassT<T>::test<T>(0)) == 1 };
    enum { No = !Yes };
};

Quando IsClassT<int>::Yesé avaliado, 0 não pode ser convertido em int int::*porque int não é uma classe e, portanto, não pode ter um ponteiro de membro. Se o SFINAE não existisse, você obteria um erro do compilador, algo como '0 não pode ser convertido em ponteiro de membro para o tipo não pertencente à classe'. Em vez disso, ele apenas usa o ...formulário que retorna Dois e, portanto, é avaliado como falso, int não é um tipo de classe.

Greg Rogers
fonte
8
@rlbond, respondi sua pergunta nos comentários a esta pergunta aqui: stackoverflow.com/questions/822059/… . Resumindo: se ambas as funções de teste são candidatas e viáveis, "..." tem o pior custo de conversão e, portanto, nunca será assumido, em favor da outra função. "..." é a elipse, coisa var-arg: int printf (char const *, ...);
Johannes Schaub - litb
O link foi alterado para blog.olivierlanglois.net/index.php/2007/09/01/…
tstenner
20
A coisa mais estranha aqui da OMI não é a ..., mas a int C::*, que eu nunca tinha visto e tive que procurar. Encontrei a resposta para o que é e para o que pode ser usado aqui: stackoverflow.com/questions/670734/…
HostileFork diz que não confia em SE
1
alguém pode explicar o que é C :: *? Eu li todos os comentários e links, mas ainda estou pensando, int C :: * significa que é um ponteiro de membro do tipo int. e se uma classe não tiver um membro do tipo int? o que estou perdendo? e como o teste <T> (0) se encaixa nisso? Devo estar faltando alguma coisa
user2584960
92

Eu gosto de usar SFINAEpara verificar condições booleanas.

template<int I> void div(char(*)[I % 2 == 0] = 0) {
    /* this is taken when I is even */
}

template<int I> void div(char(*)[I % 2 == 1] = 0) {
    /* this is taken when I is odd */
}

Pode ser bastante útil. Por exemplo, usei-o para verificar se uma lista de inicializadores coletada usando vírgula do operador não tem mais que um tamanho fixo

template<int N>
struct Vector {
    template<int M> 
    Vector(MyInitList<M> const& i, char(*)[M <= N] = 0) { /* ... */ }
}

A lista é aceita apenas quando M é menor que N, o que significa que a lista de inicializadores não possui muitos elementos.

A sintaxe char(*)[C]significa: Ponteiro para uma matriz com o tipo de elemento char e size C. Se Cfor falso (0 aqui), obtemos o tipo inválido char(*)[0], ponteiro para uma matriz de tamanho zero: SFINAE faz com que o modelo seja ignorado.

Expressado com boost::enable_if, que se parece com isso

template<int N>
struct Vector {
    template<int M> 
    Vector(MyInitList<M> const& i, 
           typename enable_if_c<(M <= N)>::type* = 0) { /* ... */ }
}

Na prática, muitas vezes considero a capacidade de verificar as condições uma habilidade útil.

Johannes Schaub - litb
fonte
1
@ Johannes Por incrível que pareça, o GCC (4.8) e o Clang (3.2) aceitam declarar matrizes de tamanho 0 (para que o tipo não seja realmente "inválido"), mas se comporta corretamente no seu código. Provavelmente, existe um suporte especial para esse caso no caso do SFINAE vs. usos "regulares" de tipos.
akim
@akim: se isso é verdade (estranho?! desde quando?), então talvez M <= N ? 1 : -1funcione.
v.oddou
1
@ v.oddou Apenas tente int foo[0]. Não estou surpreso com o suporte, pois permite o truque "estrutura que termina com uma matriz de 0 comprimento" muito útil ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html ).
akim
@akim: sim, é o que eu pensava -> C99. Esta não é permitido em C ++, aqui é o que você começa com um compilador moderno:error C2466: cannot allocate an array of constant size 0
v.oddou
1
@ v.oddou Não, eu realmente quis dizer C ++ e, na verdade, C ++ 11: ambos clang ++ e g ++ aceitam, e apontei para uma página que explica por que isso é útil.
akim
16

No C ++ 11, os testes SFINAE tornaram-se muito mais bonitos. Aqui estão alguns exemplos de usos comuns:

Escolha uma sobrecarga de função dependendo das características

template<typename T>
std::enable_if_t<std::is_integral<T>::value> f(T t){
    //integral version
}
template<typename T>
std::enable_if_t<std::is_floating_point<T>::value> f(T t){
    //floating point version
}

Usando um idioma de coletor de tipo, você pode fazer testes bastante arbitrários em um tipo como verificar se ele tem um membro e se esse membro é de um determinado tipo

//this goes in some header so you can use it everywhere
template<typename T>
struct TypeSink{
    using Type = void;
};
template<typename T>
using TypeSinkT = typename TypeSink<T>::Type;

//use case
template<typename T, typename=void>
struct HasBarOfTypeInt : std::false_type{};
template<typename T>
struct HasBarOfTypeInt<T, TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>> :
    std::is_same<typename std::decay<decltype(std::declval<T&>().*(&T::bar))>::type,int>{};


struct S{
   int bar;
};
struct K{

};

template<typename T, typename = TypeSinkT<decltype(&T::bar)>>
void print(T){
    std::cout << "has bar" << std::endl;
}
void print(...){
    std::cout << "no bar" << std::endl;
}

int main(){
    print(S{});
    print(K{});
    std::cout << "bar is int: " << HasBarOfTypeInt<S>::value << std::endl;
}

Aqui está um exemplo ao vivo: http://ideone.com/dHhyHE Também recentemente escrevi uma seção inteira sobre SFINAE e envio de tags no meu blog (plug descarado, mas relevante) http://metaporky.blogspot.de/2014/08/ part-7-static-dispatch-function.html

Observe que no C ++ 14 existe um std :: void_t que é essencialmente o mesmo que o meu TypeSink aqui.

odinthenerd
fonte
Seu primeiro bloco de código redefine o mesmo modelo.
TC
Como não existe um tipo para o qual is_integral e is_floating_point sejam verdadeiros, deve ser um ou porque SFINAE removerá pelo menos um.
precisa saber é o seguinte
Você está redefinindo o mesmo modelo com diferentes argumentos de modelo padrão. Você já tentou compilar?
TC
2
Eu sou novo na metaprogramação de modelos, então eu queria entender este exemplo. Existe uma razão para você usar TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>em um lugar e depois TypeSinkT<decltype(&T::bar)>em outro? Também é &necessário em std::declval<T&>?
precisa
1
Sobre o seu TypeSink, C ++ 17 têm std::void_t:)
YSC
10

A biblioteca enable_if do Boost oferece uma interface limpa e agradável para o uso do SFINAE. Um dos meus exemplos de uso favoritos está na biblioteca Boost.Iterator . SFINAE é usado para habilitar conversões do tipo iterador.

David Joyner
fonte
4

O C ++ 17 provavelmente fornecerá um meio genérico para consultar recursos. Consulte N4502 para obter detalhes, mas como um exemplo independente, considere o seguinte.

Esta parte é a parte constante, coloque-a em um cabeçalho.

// See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4502.pdf.
template <typename...>
using void_t = void;

// Primary template handles all types not supporting the operation.
template <typename, template <typename> class, typename = void_t<>>
struct detect : std::false_type {};

// Specialization recognizes/validates only types supporting the archetype.
template <typename T, template <typename> class Op>
struct detect<T, Op, void_t<Op<T>>> : std::true_type {};

O exemplo a seguir, retirado do N4502 , mostra o uso:

// Archetypal expression for assignment operation.
template <typename T>
using assign_t = decltype(std::declval<T&>() = std::declval<T const &>())

// Trait corresponding to that archetype.
template <typename T>
using is_assignable = detect<T, assign_t>;

Comparado com as outras implementações, esta é bastante simples: basta um conjunto reduzido de ferramentas ( void_te detect). Além disso, foi relatado (consulte a N4502 ) que é mensuravelmente mais eficiente (tempo de compilação e consumo de memória do compilador) do que as abordagens anteriores.

Aqui está um exemplo ao vivo , que inclui ajustes de portabilidade para o GCC pré 5.1.

akim
fonte
3

Aqui está outra (tarde) SFINAE exemplo, com base em Greg Rogers 's resposta :

template<typename T>
class IsClassT {
    template<typename C> static bool test(int C::*) {return true;}
    template<typename C> static bool test(...) {return false;}
public:
    static bool value;
};

template<typename T>
bool IsClassT<T>::value=IsClassT<T>::test<T>(0);

Dessa maneira, você pode verificar o valuevalor de para ver se Té uma classe ou não:

int main(void) {
    std::cout << IsClassT<std::string>::value << std::endl; // true
    std::cout << IsClassT<int>::value << std::endl;         // false
    return 0;
}
whoan
fonte
O que essa sintaxe int C::*na sua resposta significa? Como pode C::*ser um nome de parâmetro?
precisa saber é o seguinte
1
É um ponteiro para o membro. Alguma referência: isocpp.org/wiki/faq/pointers-to-members
whoan
@KirillKobelev int C::*é o tipo de um ponteiro para uma intvariável de membro de C.
YSC
3

Aqui está um bom artigo do SFINAE: Uma introdução ao conceito SFINAE do C ++: introspecção em tempo de compilação de um membro da classe .

Resuma da seguinte forma:

/*
 The compiler will try this overload since it's less generic than the variadic.
 T will be replace by int which gives us void f(const int& t, int::iterator* b = nullptr);
 int doesn't have an iterator sub-type, but the compiler doesn't throw a bunch of errors.
 It simply tries the next overload. 
*/
template <typename T> void f(const T& t, typename T::iterator* it = nullptr) { }

// The sink-hole.
void f(...) { }

f(1); // Calls void f(...) { }

template<bool B, class T = void> // Default template version.
struct enable_if {}; // This struct doesn't define "type" and the substitution will fail if you try to access it.

template<class T> // A specialisation used if the expression is true. 
struct enable_if<true, T> { typedef T type; }; // This struct do have a "type" and won't fail on access.

template <class T> typename enable_if<hasSerialize<T>::value, std::string>::type serialize(const T& obj)
{
    return obj.serialize();
}

template <class T> typename enable_if<!hasSerialize<T>::value, std::string>::type serialize(const T& obj)
{
    return to_string(obj);
}

declvalé um utilitário que fornece uma "referência falsa" a um objeto de um tipo que não pode ser facilmente construído. declvalé realmente útil para nossas construções SFINAE.

struct Default {
    int foo() const {return 1;}
};

struct NonDefault {
    NonDefault(const NonDefault&) {}
    int foo() const {return 1;}
};

int main()
{
    decltype(Default().foo()) n1 = 1; // int n1
//  decltype(NonDefault().foo()) n2 = n1; // error: no default constructor
    decltype(std::declval<NonDefault>().foo()) n2 = n1; // int n2
    std::cout << "n2 = " << n2 << '\n';
}
zangw
fonte
0

Aqui, estou usando a sobrecarga de função de modelo (não diretamente SFINAE) para determinar se um ponteiro é uma função ou ponteiro de classe de membro: ( É possível corrigir os ponteiros de função de membro costro / cerr iostream que estão sendo impressos como 1 ou verdadeiro? )

https://godbolt.org/z/c2NmzR

#include<iostream>

template<typename Return, typename... Args>
constexpr bool is_function_pointer(Return(*pointer)(Args...)) {
    return true;
}

template<typename Return, typename ClassType, typename... Args>
constexpr bool is_function_pointer(Return(ClassType::*pointer)(Args...)) {
    return true;
}

template<typename... Args>
constexpr bool is_function_pointer(Args...) {
    return false;
}

struct test_debugger { void var() {} };
void fun_void_void(){};
void fun_void_double(double d){};
double fun_double_double(double d){return d;}

int main(void) {
    int* var;

    std::cout << std::boolalpha;
    std::cout << "0. " << is_function_pointer(var) << std::endl;
    std::cout << "1. " << is_function_pointer(fun_void_void) << std::endl;
    std::cout << "2. " << is_function_pointer(fun_void_double) << std::endl;
    std::cout << "3. " << is_function_pointer(fun_double_double) << std::endl;
    std::cout << "4. " << is_function_pointer(&test_debugger::var) << std::endl;
    return 0;
}

Impressões

0. false
1. true
2. true
3. true
4. true

Como o código é, ele pode (dependendo do compilador "bom") gerar uma chamada em tempo de execução para uma função que retornará verdadeiro ou falso. Se você deseja forçar a is_function_pointer(var)avaliação no tipo de compilação (nenhuma chamada de função é executada em tempo de execução), você pode usar o constexprtruque de variável:

constexpr bool ispointer = is_function_pointer(var);
std::cout << "ispointer " << ispointer << std::endl;

Pelo padrão C ++, todas as constexprvariáveis ​​são garantidas para serem avaliadas em tempo de compilação ( comprimento de computação de uma cadeia C em tempo de compilação. Isso é realmente um constexpr? ).

do utilizador
fonte
0

O código a seguir usa SFINAE para permitir que o compilador selecione uma sobrecarga com base no fato de um tipo ter determinado método ou não:

    #include <iostream>
    
    template<typename T>
    void do_something(const T& value, decltype(value.get_int()) = 0) {
        std::cout << "Int: " <<  value.get_int() << std::endl;
    }
    
    template<typename T>
    void do_something(const T& value, decltype(value.get_float()) = 0) {
        std::cout << "Float: " << value.get_float() << std::endl;
    }
    
    
    struct FloatItem {
        float get_float() const {
            return 1.0f;
        }
    };
    
    struct IntItem {
        int get_int() const {
            return -1;
        }
    };
    
    struct UniversalItem : public IntItem, public FloatItem {};
    
    int main() {
        do_something(FloatItem{});
        do_something(IntItem{});
        // the following fails because template substitution
        // leads to ambiguity 
        // do_something(UniversalItem{});
        return 0;
    }

Resultado:

Flutuador: 1
Int: -1
vaqueiro
fonte