SFINAE的好用途是什么?

173
我想更深入地了解模板元编程。我知道SFINAE代表“替换失败不是错误”。但有人能给我展示一个SFINAE的好用法吗?

4
这是一个很好的问题。我对SFINAE非常了解,但我认为我从未必须使用它(除非库在我不知情的情况下使用它)。 - Zifre
10个回答

112

我喜欢使用 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为false(此处为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) { /* ... */ }
}

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

4
有趣的是,GCC(4.8)和Clang(3.2)允许声明大小为0的数组(因此类型并不真正“无效”),但在您的代码中它会正常工作。在SFINAE与类型的“常规”用法之间可能存在对这种情况的特殊支持。 - akim
1
@akim:如果那是真的(奇怪?!从什么时候开始?),那么也许 M <= N ? 1 : -1 可以起作用。 - v.oddou
2
@v.oddou 只需要尝试 int foo[0]。我并不惊讶它被支持,因为它允许非常有用的“以0长度数组结尾的结构体技巧”(https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html)。 - akim
@akim:是的,这就是我想的->C99。在C++中不允许这样做,现代编译器会给你这样的结果:error C2466: cannot allocate an array of constant size 0 - v.oddou
4
不,我真的是指C++,实际上是C++11:clang++和g++都支持它,并且我已经指出了一个解释为什么这很有用的页面。 - akim

84
这是一个例子(来自这里):
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时,0无法转换为int int::*,因为int不是一个类,所以它不能拥有成员指针。如果没有SFINAE,那么您将会得到编译器错误,类似于“0不能转换为非类类型int的成员指针”。相反,它只使用返回Two的...形式,从而评估为false,int不是一个类类型。

8
@rlbond,我在这个问题的评论中回答了你的问题:https://dev59.com/uHRA5IYBdhLWcg3wzhNY 。简而言之:如果两个测试函数都是候选的且可行的,则“...”具有最差的转换成本,因此不会被选择,而是选择另一个函数。“…”代表省略号,即变参列表,例如int printf(char const*,...);。 - Johannes Schaub - litb
30
在我看来,这里更奇怪的不是 ...,而是 int C::*,我之前从未见过它,不得不去查找。在这里找到了关于它是什么以及可能用于什么的答案:https://dev59.com/n3RB5IYBdhLWcg3wSVcI - HostileFork says dont trust SE
1
有人能解释一下 C::* 是什么吗?我看了所有的评论和链接,但仍然不清楚,int C::* 意味着它是 int 类型的成员指针。如果一个类没有 int 类型的成员,那怎么办?我漏掉了什么?test<T>(0) 又是如何起作用的?我必须漏掉了什么。 - user2584960
你能解释一下为什么在这个代码中使用了模板:template<typename C> static Two test(...);吗? - user6547518
让我感到惊讶的是,因为在这个页面的末尾 https://en.cppreference.com/w/cpp/language/sfinae,有一个相似的例子没有使用它。 - user6547518
显示剩余2条评论

23
在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相同。

你的第一个代码块重新定义了同一个模板。 - T.C.
由于不存在既具有is_integral特性又具有is_floating_point特性的类型,因此应该是二者之一,因为SFINAE将至少删除一个。 - odinthenerd
你正在使用不同的默认模板参数重新定义相同的模板。你尝试过编译它吗? - T.C.
啊,我真是太蠢了,谢谢你指出来!我已经修复了。 - odinthenerd
3
我还不熟悉模板元编程,所以我想理解这个例子。你在一个地方使用了TypeSinkT <decltype(std :: declval <T&>()。*(&T :: bar))>,然后在另一个地方使用了TypeSinkT <decltype(&T :: bar)>,这是有原因的吗?此外,在std :: declval <T&>中是否需要使用& - Kevin Doyon
3
关于你的TypeSink,C++17有std::void_t :) - YSC

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

6
其他答案提供的示例对我来说似乎比必要的更复杂。 这里是来自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

正如您所看到的,在test的第三次调用中,替换失败而没有出现错误。

5

这里有一个(较晚的)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
2
这是一个成员指针。参考链接:https://isocpp.org/wiki/faq/pointers-to-members - whoan
@KirillKobelev int C::* 是指向 C 类中的 int 成员变量的指针类型。 - YSC

4

以下是一篇关于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';
}

4
下面的代码使用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

3
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之前版本的可移植性调整。

这个答案现在已经过时了。这个功能没有被包含在C++17和C++20中,requires已经在很大程度上取代了它。 - undefined

1
在这里,我使用模板函数重载(而不是直接使用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?)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接