Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CPP SFINAE - spirits away #11887

Open
guevara opened this issue Nov 13, 2024 · 0 comments
Open

CPP SFINAE - spirits away #11887

guevara opened this issue Nov 13, 2024 · 0 comments

Comments

@guevara
Copy link
Owner

guevara commented Nov 13, 2024

CPP SFINAE - spirits away



https://ift.tt/W01Bc2l






SFINAE是C++里面的术语,确切名称叫做Substitution failure is not an error 。其实准确的来说还应该加入一个前缀template,其意义从其名称中就可以看出来:模板参数替代失败并非错误。SFINAE是一种编程技巧,利用了函数模板的重载决议(overload resolution):当有多个同名模板函数可用时,单独的一个模板函数匹配失败并不意味着错误,只有当所有的匹配都失败的时候才是错误。只用文字描述的话,SFINAE不是很好理解,后面将给出具体事例及代码说明。

下面给出利用SFINAE实现编译期内部类型判断的最简单的例子:

#include <iostream>
using namespace std;
struct Test
{
    typedef int foo;
};
template <typename T> void f(typename T::foo a)
{
    cout << "with T::foo" << endl;
} // Definition #1
template <typename T> void f(T a)
{
    cout << "without T::foo" << endl;
}                // Definition #2
int main()
{
    f<Test>(10); // Call #1. cout with T::foo
    f<int>(10);  // Call #2. cout without T::foo Without error (even though there is no int::foo) thanks to SFINAE.
}

模板函数f有两个定义,第一个定义要求类型struct Test中定义了一个类型foo,第二个定义则对参数类型不做任何要求。所以当我们显示实例化f<Test>时,第一个定义匹配成功,调用第一个定义;而当显示实例化f<int>时,第一个定义匹配失败,尝试匹配第二个定义,并最终调用第二个定义。事实上,这两个定义的先后顺序并不影响最后的结果。上面的代码的主要功能就是通过SFINAE,判断模板参数的类型是否内部定义了一个foo类型。类似的,我们可以通过下面的代码来判断传入参数类型是否内部定义了foobar类型:

#include <iostream>
template <typename T>
struct has_typedef_foobar 
{
    // Types "yes" and "no" are guaranteed to have different sizes,
    // specifically sizeof(yes) == 1 and sizeof(no) == 2.
    typedef char yes[1];
    typedef char no[10];
    template <typename C>
    static yes& test(typename C::foobar*);
    template <typename>
    static no& test(...);
    // If the "sizeof" of the result of calling test<T>(0) would be equal to sizeof(yes),
    // the first overload worked and T has a nested type named foobar.
    static const bool value = sizeof(test<T>(0)) == sizeof(yes);
};
struct foo
{    
    typedef float foobar;
};
int main() 
{
    std::cout << std::boolalpha;
    std::cout << has_typedef_foobar<int>::value << std::endl;
    std::cout << has_typedef_foobar<foo>::value << std::endl;
}

如果类型T内部定义了类型foobar,则has_typedef_foobar<int>::value的值为true,否则为false。这里之所以将no定义为10个char,是因为在不同的平台上可能有不同的内存对齐要求。如果定义为3个的话,遇到4字节对齐的硬件结构会导致sizeof(yes)sizeof(no)都返回4,在8字节对齐的机器上也是同理。目前来说还没见过大于8字节的对齐(当然自己犯贱#pragma_pack(16)的除外),所以定义为10基本可以保证这两个类型返回的字节大小是不一样的。

SFINAE的功能不仅仅是能编译期确定类型内部是否定义了新类型,而且还能在编译期确定实参类型内部是否定义了某个成员函数,样例代码如下(注意 VS编译不过。。。):

#include <type_traits>
#include <utility>
template <typename T, typename = void>
struct has_f : std::false_type { };
template <typename T>
struct has_f<T,
    decltype(std::declval<T>().f(), void())> : std::true_type { };
template <typename T, typename = typename std::enable_if<has_f<T>::value>::type> struct A { };
struct B
{
    void f();
};
struct C { };
template class A<B>; // compiles
template class A<C>; // error: no type named ‘type’ 
                     // in ‘struct std::enable_if<false, void>’

首先来理解一下std::declval,这是在C++11中引入的一个函数模板,具体定义如下:

template< class T >
typename std::add_rvalue_reference<T>::type declval();

其具体作用就是返回任意类型T的一个右值引用,即使该类型不存在构造函数。但是该右值引用不可以用来求值,只能用在不可求值环境,只能用来推导类型,如其成员变量和成员函数的类型。而decltype(std::declval<T>().f(), void())的作用就是强制进行std::declval<T>().f()的类型推导,并最后返回void类型。

std::false_type,std::true_type内部都有一个成员value,类似于我们上个事例代码中的has_typedef_foobar<int>::value,值分别为false,true。而std::enable_if是一个类模板,并显示特化为了true,false两种类型。std::enable_if<true>内部定义了一个新类型type,而std::enable_if<false>内部则没有定义这个类型。std::enable_if具体定义代码如下所示:

template<bool B, class T = void>
struct enable_if {};
template<class T>
struct enable_if<true, T> { typedef T type; };

在这一系列的模板特化和实例化之下,语句A<B>能够编译通过,而语句A(C)则编译报错。

当前我们只做到了判断没有参数也没有返回值的函数的存在性,事实上还可以判断具体签名的成员函数的存在性。假设我们要判断某个类内部是否定义了size_t used_memory() const这个函数,该需求可以通过下面的代码来实现:

template<typename T>
struct HasUsedMemoryMethod
{
    template<typename U, size_t (U::*)() const> struct SFINAE {};
    template<typename U> static char Test(SFINAE<U, &U::used_memory>*);
    template<typename U> static int Test(...);
    static const bool Has = sizeof(Test<T>(0)) == sizeof(char);
};

template<typename TMap>
void ReportMemUsage(const TMap& m, std::true_type)
{
// We may call used_memory() on m here.
}
template<typename TMap>
void ReportMemUsage(const TMap&, std::false_type)
{
}
template<typename TMap>
void ReportMemUsage(const TMap& m)
{
ReportMemUsage(m, std::integral_constant<bool, HasUsedMemoryMethod<TMap>::Has>());
}

这里用到了成员指针::*这个类型,因为普通指针无法指向成员函数(多了一个this指针的参数),具体细节读者自行百度吧。由于个人能力所限,还无法得到有参数的成员函数的存在性判断,但是现在最起码得到了有返回值类型的成员函数的存在性判断,先偷着乐一会吧。

对于一个变量,我们还可以利用SFINAE来判断该变量是否是指针类型的:包括普通变量指针,成员变量指针,成员函数指针,函数指针这四种指针类型。type_traits中的is_pointer元函数可以做到这一功能,代码如下:

template <typename T>
struct is_pointer
{
  template <typename U>
  static char is_ptr(U *);

template <typename X, typename Y>
static char is_ptr(Y X::*);

template <class U>
static char is_ptr(U (*)());

static double is_ptr(...);

static T t;
enum { value = sizeof(is_ptr(t)) == sizeof(char) };
};

struct Foo {
int bar;
};

int main(void)
{
typedef int IntPtr;
typedef int Foo::
FooMemberPtr;
typedef int (*FuncPtr)();

printf("%d\n",is_pointer<IntPtr>::value); // prints 1
printf("%d\n",is_pointer<FooMemberPtr>::value); // prints 1
printf("%d\n",is_pointer<FuncPtr>::value); // prints 1
return 0;
}

上面的代码通过显示特化了所有的指针类型并让函数模板返回值为char,同时让非指针类型的函数模板特化为返回double。这两个返回类型在sizeof运算符下返回的值是不同的,所以我们可以把指针类型与非指针类型区分出来。

除了上面提到的类内部类型和类函数存在性判断之外,我们还可以判断某个类A是否是从B继承下来的。事例代码如下:

template<class A, class B>
class IsDerivedFrom
{
private:
  class Yes { char a[1]; };
  class No { char a[10]; };

static Yes Test( B* ); // undefined
static No Test( ... ); // undefined

public:
enum { Is = sizeof(Test(static_cast<A*>(0))) == sizeof(Yes) ? 1 : 0 };
};

static_cast<D*>(0)这一句的作用是将0转换为一个A类型的指针。由于Test(B*)接受的是B*类型的参数,所以会默认进行A*B*的转换,如果可以转换,则匹配第一个模板。如果A*没有到B*的转换,则Test(B*)这个模板匹配失败,并最终匹配了第二个模板。剩下的工作就好理解了,这里就不再赘述。需要注意的是,这里并不是去做真正的类型转换,而且,只有enumconst static类型的值才能在编译期动态赋值。

上面的代码只能实现可转换的判断,如果AB的类型相同的时候,会返回1。如果我们要判断的是A是否是B的继承类型的时候,该结果并不令人满意。所以我们还需要增加一个代码片段,来排除相同类型的情况。代码示例如下:

template<class T>
class IsDerivedFrom<T,T>
{
public:
  enum { Is = 0 };
};

上面的两个代码合并起来,就能完美的判断是否是继承类型的问题。

我们也可以利用SFINAE来判断一个类是不是纯虚类。此时我们使用到了纯虚类的一个性质:不能声明纯虚类的变量。所以对于纯虚类A来说,A(*)[1]是无法实例化的。因为这个类型是一个指向A类型的数组的指针(注意不是A指针的数组),而数组声明要求类型不能为reference,void,function,abstract。所以我们可以用以下的代码测试类型是不是纯虚类:

template <typename T>
struct IsAbstract
{
    typedef char SmallType;
    typedef int LargeType;
<span>template</span> <span>&lt;</span><span>typename</span> <span>U</span><span>&gt;</span>
<span>static</span> <span>char</span> <span>Test</span><span>(</span><span>U</span><span>(</span><span>*</span><span>)[</span><span>1</span><span>]);</span>
<span>template</span> <span>&lt;</span><span>typename</span> <span>U</span><span>&gt;</span>
<span>static</span> <span>int</span> <span>Test</span><span>(...);</span>

<span>const</span> <span>static</span> <span>bool</span> <span>Result</span> <span>=</span> <span>sizeof</span><span>(</span><span>Test</span><span>&lt;</span><span>T</span><span>&gt;</span><span>(</span><span>NULL</span><span>))</span> <span>==</span> <span>sizeof</span><span>(</span><span>LargeType</span><span>);</span>

};

但是,正如前文所说,reference,void,function都会导致类型被判断为纯虚类,所以应该再对这几种类型做特化,这里就不写了。

虽然之前的两个例子让我们感到了模板与SFINAE的强大与恐怖之处,但是得到了判断结果又能怎么样呢,好像都没有多大实际作用的样子。这里,我们给出一个有实际作用的例子:利用SFINAE实现类模板特化。

假设我们当前要实现一个容器C<T>,用来存储T类型的值。一个最简单的实现如下:

template <typename T>
class C 
{
  private:
    T t;
  public:
    C(const C& rhs);
    C(C&& rhs)
  // other stuff
};

由于有些类型的T是不可拷贝的,例如std::mutex,std::unique_ptr。但是对于这些类型的Tis_copy_constructible<C<T>\,>::value仍然是true的。为了防止这些类型的拷贝构造,我们要把C<T>的拷贝构造函数禁用。代码如下:

template<bool copyable>
struct copyable_characteristic { };

template<>
struct copyable_characteristic<false> {
copyable_characteristic() = default;
copyable_characteristic(const copyable_characteristic&) = delete;
};

template <typename T> class C
: copyable_characteristic<std::is_copy_constructible<T>::value>
{
public:
C(const C&) = default;
C(C&& rhs);
// other stuff
};

因为默认的拷贝构造函数会首先调用父类的拷贝构造函数。当父类的拷贝构造函数为delete的时候,子类的拷贝构造函数也就相当于声明为了delete,这个解决方案简直完美。

模板参数既能决定类的行为,同时也能决定函数的行为。如下例:

template<int N> struct A
{
public:
    int sum() const 
    { return _sum<N - 1>(); }
    template <int otherN, typename = typename std::enable_if<otherN >= N>::type>
    explicit A(A<otherN> const &)
    {
<span>}</span>
<span>A</span><span>()</span> <span>=</span> <span>default</span><span>;</span>

private:
int _data[N];
template<int I> typename std::enable_if< I, int>::type _sum() const
{ return _sum<I - 1>() + _data[I]; }
template<int I> typename std::enable_if<!I, int>::type _sum() const
{ return _data[I]; }
};
int main()
{
A<4> a4;
A<5> a5;
A<3> a3(a5);
A<7> a7(a3); //error
return 1;
}

上面的代码中,_sum<N>是一个模板元函数,A<N>是一个封装了N个整数的结构体。_sum<N>非常巧妙的利用了intbool的隐式类型转换,以及std::enable_if的特性,还有类型别名机制,实现了递归求前N个元素和的壮举。同时,A(A<otherN> const)也通过利用std::enable_if实现了把拷贝构造中长度控制的奇迹。看到这两个简短而又不知所云的代码是不是感觉有点怕!当SFINAE和TYPETRIATS一起结合的时候,the real terror awaits you

</div>






via spiritsaway.info https://ift.tt/xtvPQyO

November 13, 2024 at 10:06AM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant