C ++ SFINAE 예제?


122

더 많은 템플릿 메타 프로그래밍에 대해 알아보고 싶습니다. SFINAE가 "대체 실패는 오류가 아닙니다"라는 뜻이라는 것을 알고 있습니다. 하지만 누군가 SFINAE에 대한 좋은 사용을 보여줄 수 있습니까?


2
이것은 좋은 질문입니다. 나는 SFINAE를 꽤 잘 이해하지만, 나는 그것을 사용해야 만했다고 생각하지 않는다 (도서관이 그것을 내가 모르게하지 않는 한).
Zifre

5
STL은 "대체 실패는 코끼리가 아닙니다" FAQ에서 약간 다르게 설명했습니다.
vulcan raven

답변:


72

다음은 한 가지 예입니다 ( 여기에서 ).

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 };
};

IsClassT<int>::Yes가 평가 되면 int int::*int가 클래스가 아니기 때문에 0으로 변환 할 수 없으므로 멤버 포인터를 가질 수 없습니다. SFINAE가 존재하지 않으면 '0을 비 클래스 유형 int의 멤버 포인터로 변환 할 수 없습니다'와 같은 컴파일러 오류가 발생합니다. 대신 ...Two를 반환 하는 형식을 사용 하므로 false로 평가되며 int는 클래스 유형이 아닙니다.


8
@rlbond, 나는이 질문에 대한 의견에 귀하의 질문에 대답했습니다 : stackoverflow.com/questions/822059/… . 간단히 말해서, 두 테스트 기능이 모두 후보이고 실행 가능한 경우 "..."는 최악의 변환 비용을 가지므로 다른 기능에 유리하게 사용되지 않습니다. "..."는 줄임표, var-arg 항목입니다. int printf (char const *, ...);
Johannes Schaub-litb


20
여기서 이상한 것은 IMO가 ...아니라 int C::*내가 본 적이없고 찾아보아야했던. 에 대한 답을 찾을 수 그 무엇이고 무엇을 여기에 사용할 수 있습니다 stackoverflow.com/questions/670734/...
HostileFork은 그나마 신뢰 SE 말한다

1
누군가 C :: *가 무엇인지 설명 할 수 있습니까? 모든 주석과 링크를 읽었지만 여전히 궁금합니다. int C :: *는 그것이 int 유형의 멤버 포인터임을 의미합니다. 클래스에 int 유형의 멤버가 없으면 어떻게됩니까? 내가 무엇을 놓치고 있습니까? 그리고 test <T> (0)은 이것에서 어떻게 작동합니까? 내가 뭔가 빠진해야
user2584960

92

SFINAE부울 조건을 확인 하는 데 사용 하는 것을 좋아 합니다.

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 */
}

매우 유용 할 수 있습니다. 예를 들어 연산자 쉼표를 사용하여 수집 한 이니셜 라이저 목록이 고정 크기보다 길지 않은지 확인하는 데 사용했습니다.

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

이 목록은 M이 N보다 작을 때만 허용됩니다. 즉, 이니셜 라이저 목록에 요소가 너무 많지 않음을 의미합니다.

구문 char(*)[C]은 다음을 의미합니다. 요소 유형이 char 및 size 인 배열에 대한 포인터 C. 경우 C(여기서 0) 거짓, 우리는 잘못된 유형 얻을 char(*)[0]크기가 0 인 배열, 포인터를 : SFINAE 템플릿은 다음 무시됩니다 있도록한다.

로 표현하면 boost::enable_if다음과 같습니다.

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

실제로 나는 종종 조건을 확인하는 능력이 유용한 능력이라고 생각합니다.


1
@Johannes 이상하게도 GCC (4.8)와 Clang (3.2)은 크기가 0 인 배열을 선언하는 것을 허용하지만 (따라서 유형이 실제로 "유효하지 않음"이 아님) 코드에서 제대로 작동합니다. SFINAE 대 "일반적인"유형 사용의 경우이 경우에 대한 특별한 지원이있을 수 있습니다.
akim

@akim : 그것이 사실이라면 (이상한?! 언제부터?) M <= N ? 1 : -1대신 작동 할 수 있습니다.
v.oddou 2014-06-13

1
@ v.oddou 그냥 시도하십시오 int foo[0]. 매우 유용한 "길이가 0 인 배열로 끝나는 구조체"트릭 ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html )을 허용하므로 지원된다는 사실에 놀랍지 않습니다 .
akim

@akim : 네, 내가 생각했던 것-> C99. 이것은 C에서 허용되지 않습니다 ++, 여기 당신이 현대 컴파일러로 무엇을 얻을 수 있습니다 :error C2466: cannot allocate an array of constant size 0
v.oddou

1
@ v.oddou 아니요, 저는 실제로 C ++를 의미했고 실제로 C ++ 11 : clang ++와 g ++ 모두 허용하고 이것이 왜 유용한 지 설명하는 페이지를 가리 켰습니다.
akim

16

C ++ 11에서는 SFINAE 테스트가 훨씬 더 예뻐졌습니다. 다음은 일반적인 사용의 몇 가지 예입니다.

특성에 따라 함수 과부하 선택

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
}

소위 유형 싱크 관용구를 사용하면 멤버가 있는지, 해당 멤버가 특정 유형인지 확인하는 것과 같이 유형에 대해 임의의 테스트를 수행 할 수 있습니다.

//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;
}

다음은 실제 예입니다. http://ideone.com/dHhyHE 최근에 SFINAE에 대한 전체 섹션을 썼고 내 블로그에 태그 디스패치를 ​​썼습니다 . part-7-static-dispatch-function.html

C ++ 14부터 std :: void_t가 여기에있는 TypeSink와 본질적으로 동일합니다.


첫 번째 코드 블록은 동일한 템플릿을 재정의합니다.
TC

is_integral과 is_floating_point가 모두 참인 유형이 없기 때문에 둘 중 하나이거나 SFINAE가 적어도 하나를 제거하기 때문입니다.
odinthenerd 2014 년

다른 기본 템플릿 인수를 사용하여 동일한 템플릿을 재정의하고 있습니다. 컴파일 해 보셨나요?
TC

2
저는 템플릿 메타 프로그래밍이 처음이어서이 예제를 이해하고 싶었습니다. TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>한 곳에서 사용 하고 다른 곳에서 사용하는 이유가 TypeSinkT<decltype(&T::bar)>있습니까? 또한 &필요 std::declval<T&>합니까?
Kevin Doyon

1
귀하의 TypeSink, C ++ 17에 대해 std::void_t:)
YSC

10

Boost의 enable_if 라이브러리는 SFINAE 사용을위한 깔끔한 인터페이스를 제공합니다. 제가 가장 좋아하는 사용 예제 중 하나는 Boost.Iterator 라이브러리입니다. SFINAE는 반복기 유형 변환을 사용하는 데 사용됩니다.


4

C ++ 17은 기능을 쿼리하는 일반적인 수단을 제공 할 것입니다. 자세한 내용은 N4502 를 참조하십시오 . 자체 포함 된 예로서 다음을 고려하십시오.

이 부분은 상수 부분이며 헤더에 넣습니다.

// 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 {};

N4502 에서 가져온 다음 예제 는 사용법을 보여줍니다.

// 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>;

다른 구현에 비해 이것은 매우 간단합니다. 축소 된 도구 세트 ( void_tdetect)로 충분합니다. 게다가 이전 접근 방식보다 훨씬 더 효율적 (컴파일 시간 및 컴파일러 메모리 소비) 이보고되었습니다 ( N4502 참조 ).

다음은 GCC 5.1 이전 버전에 대한 이식성 조정을 포함 하는 라이브 예제 입니다.


3

Greg Rogers답변을 기반으로 한 또 다른 (늦은) SFINAE 예가 있습니다 .

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);

이런 식으로 value의 값을 확인 T하여 클래스 인지 여부를 확인할 수 있습니다 .

int main(void) {
    std::cout << IsClassT<std::string>::value << std::endl; // true
    std::cout << IsClassT<int>::value << std::endl;         // false
    return 0;
}

int C::*답변 에서이 구문 은 무엇을 의미합니까? C::*매개 변수 이름 은 어떻게 될 수 있습니까?
Kirill Kobelev

1
회원에 대한 포인터입니다. 일부 참조 : isocpp.org/wiki/faq/pointers-to-members
whoan

@KirillKobelev int C::*int멤버 변수에 대한 포인터 유형 입니다 C.
YSC

3

다음은 SFINAE의 좋은 기사입니다. C ++의 SFINAE 개념 소개 : 클래스 멤버의 컴파일 시간 내부 검사 .

다음과 같이 요약하십시오.

/*
 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쉽게 구성 할 수없는 유형의 객체에 대한 "가짜 참조"를 제공하는 유틸리티입니다. declvalSFINAE 구조에 정말 편리합니다.

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';
}

0

여기에서는 포인터가 함수인지 멤버 클래스 포인터인지 확인하기 위해 템플릿 함수 오버로딩 (직접 SFINAE가 아님)을 사용하고 있습니다. ( 1 또는 true로 인쇄되는 iostream cout / cerr 멤버 함수 포인터를 수정할 수 있습니까? )

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;
}

인쇄물

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

코드이기 때문에, 그것은 true 또는 false를 반환하는 함수에 대한 런타임 호출을 생성 (것 컴파일러 "좋은"에 따라). is_function_pointer(var)컴파일 유형에서 평가 하도록 강제하려면 (런타임에 함수 호출이 수행되지 않음) constexpr변수 트릭을 사용할 수 있습니다 .

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

C ++ 표준에 따라 모든 constexpr변수는 컴파일 시간에 평가됩니다 (컴파일 시간 에 C 문자열의 길이 계산. 이것이 실제로 constexpr입니까? ).


0

다음 코드는 SFINAE를 사용하여 컴파일러가 형식에 특정 메서드가 있는지 여부에 따라 오버로드를 선택할 수 있도록합니다.

    #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;
    }

산출:

플로트 : 1
정수 : -1
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.