C++ SFINAE 示例?

Posted

技术标签:

【中文标题】C++ SFINAE 示例?【英文标题】:C++ SFINAE examples? 【发布时间】:2010-11-02 05:45:29 【问题描述】:

我想学习更多的模板元编程。我知道 SFINAE 代表“替代失败不是错误”。但是有人可以告诉我 SFINAE 的好用处吗?

【问题讨论】:

这是个好问题。我非常了解 SFINAE,但我认为我从未使用过它(除非图书馆在我不知情的情况下使用它)。 【参考方案1】:

我喜欢使用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 且大小为C 的数组的指针。如果C 为假(此处为0),那么我们会得到无效类型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)  /* ... */ 

在实践中,我经常发现检查条件的能力很有用。

【讨论】:

@Johannes 奇怪的是,GCC (4.8) 和 Clang (3.2) 接受声明大小为 0 的数组(因此该类型并不是真正“无效”),但它在您的代码中表现正常。在 SFINAE 与“常规”使用类型的情况下,这种情况可能会得到特殊支持。 @akim:如果这是真的(奇怪?!从什么时候开始?)那么也许M &lt;= N ? 1 : -1 可以代替。 @v.oddou 试试int foo[0]。我对它的支持并不感到惊讶,因为它允许非常有用的“结构以 0 长度数组结尾”技巧 (gcc.gnu.org/onlinedocs/gcc/Zero-Length.html)。 @akim:是的,这就是我的想法-> C99。这在 C++ 中是不允许的,这是现代编译器所得到的:error C2466: cannot allocate an array of constant size 0 @v.oddou 不,我的意思是 C++,实际上是 C++11:clang++ 和 g++ 都接受它,我已经指出了一个解释为什么这很有用的页面。【参考方案2】:

这是一个例子 (from here):

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&lt;int&gt;::Yes求值时,0不能转换为int int::*,因为int不是类,所以不能有成员指针。如果 SFINAE 不存在,那么您会收到编译器错误,例如“0 无法转换为非类类型 int 的成员指针”。相反,它只使用返回 Two 的 ... 形式,因此计算结果为 false,int 不是类类型。

【讨论】:

@rlbond,我在 cmets 中回答了您对这个问题的提问:***.com/questions/822059/…。简而言之:如果两个测试函数都是候选函数并且可行,那么“...”具有最差的转换成本,因此永远不会被采用,有利于另一个函数。 "..." 是省略号,var-arg 的东西: int printf(char const*, ...); 链接改为blog.olivierlanglois.net/index.php/2007/09/01/… IMO 更奇怪的不是...,而是int C::*,我从未见过,不得不去查找。在这里找到了它是什么以及它可能用于什么的答案:***.com/questions/670734/… 有人能解释一下 C::* 是什么吗?我阅读了所有的 cmets 和链接,但我仍然想知道, int C::* 意味着它是 int 类型的成员指针。如果一个类没有 int 类型的成员怎么办?我错过了什么?以及 test(0) 如何发挥作用?我一定是错过了什么 你能解释一下为什么你在这个中使用模板: template static 两个 test(...); ?【参考方案3】:

在 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/part-7-static-dispatch-function.html

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

【讨论】:

您的第一段代码重新定义了同一个模板。 因为没有一个类型的 is_integral 和 is_floating_point 都为真,所以它应该是一个或者因为 SFINAE 将删除至少一个。 您正在使用不同的默认模板参数重新定义相同的模板。你试过编译吗? 我是模板元编程的新手,所以我想了解这个例子。您是否有理由在一个地方使用TypeSinkT&lt;decltype(std::declval&lt;T&amp;&gt;().*(&amp;T::bar))&gt;,然后在另一个地方使用TypeSinkT&lt;decltype(&amp;T::bar)&gt;&amp; 中的std::declval&lt;T&amp;&gt; 也是必需的吗? 关于你的TypeSink,C++17 有std::void_t :)【参考方案4】:

Boost 的enable_if 库为使用 SFINAE 提供了一个非常干净的界面。我最喜欢的使用示例之一是在 Boost.Iterator 库中。 SFINAE 用于启用迭代器类型转换。

【讨论】:

【参考方案5】:

C++17 可能会提供一种通用的方法来查询特性。有关详细信息,请参阅 N4502,但作为一个独立的示例,请考虑以下内容。

这部分是常量部分,放在header里面。

// 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)它比以前的方法更有效(编译时间和编译器内存消耗)。

这是一个live example,其中包括 GCC pre 5.1 的可移植性调整。

【讨论】:

【参考方案6】:

这是另一个(迟到的)SFINAE 示例,基于Greg Rogers 的answer:

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::*怎么可能是参数名? 它是一个指向成员的指针。一些参考:isocpp.org/wiki/faq/pointers-to-members @KirillKobelev int C::* 是指向Cint 成员变量的指针类型。【参考方案7】:

这是 SFINAE 的一篇好文章:An introduction to C++'s SFINAE concept: compile-time introspection of a class member。

总结如下:

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

【讨论】:

【参考方案8】:

以下代码使用 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

【讨论】:

【参考方案9】:

其他答案提供的示例在我看来比需要的更复杂。

下面是来自cppreference 的更容易理解的示例:

#include <iostream>
 
// this overload is always in the set of overloads
// ellipsis parameter has the lowest ranking for overload resolution
void test(...)

    std::cout << "Catch-all overload called\n";

 
// this overload is added to the set of overloads if
// C is a reference-to-class type and F is a pointer to member function of C
template <class C, class F>
auto test(C c, F f) -> decltype((void)(c.*f)(), void())

    std::cout << "Reference overload called\n";

 
// this overload is added to the set of overloads if
// C is a pointer-to-class type and F is a pointer to member function of C
template <class C, class F>
auto test(C c, F f) -> decltype((void)((c->*f)()), void())

    std::cout << "Pointer overload called\n";

 
struct X  void f()  ;
 
int main()
  X x;
  test( x, &X::f);
  test(&x, &X::f);
  test(42, 1337);

输出:

Reference overload called
Pointer overload called
Catch-all overload called

如您所见,在第三次测试调用中,替换失败且没有错误。

【讨论】:

【参考方案10】:

这里,我是使用模板函数重载(不是直接SFINAE)来判断一个指针是函数还是成员类指针:(Is possible to fix the iostream cout/cerr member function pointers being printed as 1 or 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 变量都保证在编译时进行评估 (Computing length of a C string at compile time. Is this really a constexpr?)。

【讨论】:

以上是关于C++ SFINAE 示例?的主要内容,如果未能解决你的问题,请参考以下文章

具有模板化类成员函数的 SFINAE

C++模板进阶指南:SFINAE

Microsoft Visual C++ 中的 SFINAE constexpr 错误

编译期多态和Sfinae

模板函数重载和 SFINAE 实现

g++ 和 clang++ 使用变量模板和 SFINAE 的不同行为