C + + SFINAE 例子?

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

64484 次浏览

这里有一个例子(从这里) :

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 不是类类型。

Boost 的 启用 _ if库为使用 SFINAE 提供了一个干净的界面。我最喜欢的用法示例之一是在 升压,迭代器库中。SFINAE 用于启用迭代器类型转换。

我喜欢使用 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) { /* ... */ }
}

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

在 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-) rel = “ norefrer”> http://metaporky.blogspot.de/2014/08/part-7-static-dispatch-function.html

注意,在 C + + 14中有一个 std: : void _ t,它实际上与这里的 TypeSink 相同。

下面是另一个(晚期) 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;
}

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 pre 5.1的可移植性调整。

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

在这里,我使用模板函数重载(不直接使用 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 字符串的长度?)进行求值。

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

产出:

Float: 1
Int: -1

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

下面是 首选中比较容易理解的例子:

#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 的第三个调用中,替换失败而没有错误。