C ++ SFINAE示例?


122

我想了解更多模板元编程。我知道SFINAE代表“替代失败不是错误”。但是有人可以向我展示SFINAE的良好用途吗?


2
这是一个很好的问题。我对SFINAE非常了解,但我认为我从来没有使用过它(除非图书馆在不知情的情况下正在使用它)。
Zifre 2009年

5
STL在“ 常见问题解答 ” 中的常见问题解答中稍有不同,“替代失败不是
一头

Answers:


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的成员指针”。取而代之的是,它只使用...返回2 的形式,并因此得出false的值,int不是类类型。


8
@rlbond,我在此处对此问题的评论中回答了您的问题:stackoverflow.com/questions/822059/…。简而言之:如果两个测试功能都是候选功能并且可行,那么“ ...”的转换成本最差,因此永远不会采用,而偏爱另一个功能。“ ...”是省略号,可变参数: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 '18

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为false(此处为0),则得到无效的type char(*)[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年

1
@ v.oddou只需尝试int foo[0]。我支持它并不奇怪,因为它允许使用非常有用的“以0长度数组结尾的结构”技巧(gcc.gnu.org/onlinedocs/gcc/Zero-Length.html)。
akim 2014年

@akim:是的,我的想法是-> C99。这在C ++中是不允许的,这是使用现代编译器得到的结果:error C2466: cannot allocate an array of constant size 0
v.oddou

1
@ v.oddou不,我的意思是C ++,实际上是C ++ 11:clang ++和g ++都接受它,并且我指向的页面解释了为什么这样做很有用。
akim 2014年

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和标签分发的完整内容(无耻的插件,但相关)http://metaporky.blogspot.de/2014/08/第7部分-static-dispatch-function.html

注意,从C ++ 14开始,这里有一个std :: void_t与我的TypeSink基本上相同。


您的第一段代码重新定义了相同的模板。
TC

由于没有is_integral和is_floating_point都为true的类型,因此应为a或因为SFINAE将删除至少一个。
odinthenerd 2014年

您正在使用不同的默认模板参数重新定义同一模板。您是否尝试过编译?
TC

2
我是模板元编程的新手,所以我想了解这个例子。您TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>在一个地方然后TypeSinkT<decltype(&T::bar)>在另一个地方使用是否有原因?还有&必要std::declval<T&>吗?
凯文·多永

1
关于您的TypeSinkC ++ 17有std::void_t:)
YSC


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

这是另一个(晚期)SFINAE示例,基于Greg Rogers答案

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 '16

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是一个实用程序,可为您提供对无法轻松构造的类型的对象的“虚假引用”。declval对于我们的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';
}

0

在这里,我正在使用模板函数重载(不是直接SFINAE)来确定指针是函数指针还是成员类指针:(是否可以将iostream cout / cerr成员函数指针固定为1或true来打印?

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.