翻译的很差,最好去看原文。
原文链接:http://jguegant.github.io/blogs/tech/sfinae-introduction.html
网上看到一篇介绍SFINAE的文章,自己听说过很多遍,但都不太确定到底是个啥东西。作者的文章写的很好,就拿来翻译一下练练手。原作者说看了Louis Dionne 的演讲"C++ Metaprogramming: A Paradigm Shift",对其中的Boost.Hana库中的 is_valid 特性特别感兴趣。应该是有感而发,有了此文。如果不知道啥是SFINAE,并且想知道,那么就去看原文吧。原文写的很好,代码里的注释很多,受益匪浅!
在解释SFINAE是啥之前,我们先来它的主要应用之一:introspection(内省)。c++不擅长在运行时检测对象的类型和属性。虽然c++有 RTTI,但RTTI除了能提供对象的类型之外,也提供不了其他有用的信息了。而动态语言在这方面就非常方便,举个python的栗子:
class A(object):
# Simply overrides the 'object.__str__' method.
def __str__(self):
return "I am a A"
class B(object):
# A custom method for my custom objects that I want to serialize.
def serialize(self):
return "I am a B"
class C(object):
def __init__(self):
# Oups! 'serialize' is not a method.
self.serialize = 0
def __str__(self):
return "I am a C"
def serialize(obj):
# Let's check if obj has an attribute called 'serialize'.
if hasattr(obj, "serialize"):
# Let's check if this 'serialize' attribute is a method.
if hasattr(obj.serialize, "__call__"):
return obj.serialize()
# Else we call the __str__ method.
return str(obj)
a = A()
b = B()
c = C()
print(serialize(a)) # output: I am a A.
print(serialize(b)) # output: I am a B.
print(serialize(c)) # output: I am a C.
可以看到,python可以非常方便的查看一个对象是都有某个属性及这个属性的类型。在上面的栗子中,就是检测有没有serialize函数,有就调用,没有就调用str函数。很强大,不是吗?c++也可以做到这些!
下面是个c++14的方法,用到了Boost.Hana 库中的 is_valid:
#include <boost/hana.hpp>
#include <iostream>
#include <string>
using namespace std;
namespace hana = boost::hana;
// Check if a type has a serialize method.
auto hasSerialize = hana::is_valid([](auto&& x) -> decltype(x.serialize()) { });
// Serialize any kind of objects.
template <typename T>
std::string serialize(T const& obj) {
return hana::if_(hasSerialize(obj), // Serialize is selected if available!
[](auto& x) { return x.serialize(); },
[](auto& x) { return to_string(x); }
)(obj);
}
// Type A with only a to_string overload.
struct A {};
std::string to_string(const A&)
{
return "I am a A!";
}
// Type B with a serialize method.
struct B
{
std::string serialize() const
{
return "I am a B!";
}
};
// Type C with a "wrong" serialize member (not a method) and a to_string overload.
struct C
{
std::string serialize;
};
std::string to_string(const C&)
{
return "I am a C!";
}
int main() {
A a;
B b;
C c;
std::cout << serialize(a) << std::endl;
std::cout << serialize(b) << std::endl;
std::cout << serialize(c) << std::endl;
}
像c++如此复杂的语言,也可以像python一样,很方便的给出serialize的解决方案。不过不像python等动态语言,在运行时获取这些信息,c++的实现是在编译期实现的,这些全靠c++编译器可以在编译期访问类型信息的特性。接下来就看看如何分别使用c++98,c++11,c++14,来创建我们自己的is_valid。
c++98的解决方案主要依赖于三个概念:overload resolution, SFINAE 和 the static behavior of sizeof.
c++中,如果像"f(obj)"这样一个函数被调用时,会触发编译器的重载解析机制--根据形参找出最合适的名叫f的函数。没啥能比一个好栗子更能说明问题了:
void f(std::string s); // int can't be convert into a string.
void f(double d); // int can be implicitly convert into a double, so this version could be selected, but...
void f(int i); // ... this version using the type int directly is even more close!
f(1); // Call f(int i);
c++中也有可以接受任何参数的sink-hole function(不知道咋翻译)。函数模版就可以接受任何类型的参数(比如 T),但是真正的black-hole function应该是variadic functions。C 的printf就是这样的函数。
std::string f(...); // Variadic functions are so "untyped" that...
template <typename T> std::string f(const T& t); // ...this templated function got the precedence!
f(1); // Call the templated function version of f.
但是一定要记住,函数模版跟variadic functions是不一样的,函数模版其实是类型安全的(编译时可以检测参数类型对不对)。
前面说了那么多,现在终于轮到主角登场了。SFINAE是Substitution Failure Is Not An Error的缩略语。substitution可以看作是用实际调用时提供的类型或值来替换模版参数的机制。如果替换后,代码变成了无效代码,编译器也不应该抛出错误,而是继续寻找其他的替换方案。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(...) { }
sizeof操作符可以在编译时返回一个类型或表达式的大小
typedef char type_test[42];
type_test& f();
// In the following lines f won't even be truly called but we can still access to the size of its return type.
// Thanks to the "fake evaluation" of the sizeof operator.
char arrayTest[sizeof(f())];
std::cout << sizeof(f()) << std::endl; // Output 42.
如果可以在编译期计算一个整型,难道不能在编译期做整型比较吗?当然可以啦!
typedef char yes; // Size: 1 byte.
typedef yes no[2]; // Size: 2 bytes.
// Two functions using our type with different size.
yes& f1();
no& f2();
std::cout << (sizeof(f1()) == sizeof(f2())) << std::endl; // Output 0.
std::cout << (sizeof(f1()) == sizeof(f1())) << std::endl; // Output 1.
现在所有的难点都搞定了,先写个hasSerialize吧:
template <class T> struct hasSerialize
{
// For the compile time comparison.
typedef char yes[1];
typedef yes no[2];
// This helper struct permits us to check that serialize is truly a method.
// The second argument must be of the type of the first.
// For instance reallyHas<int, 10> would be substituted by reallyHas<int, int 10> and works!
// reallyHas<int, &C::serialize> would be substituted by reallyHas<int, int &C::serialize> and fail!
// Note: It only works with integral constants and pointers (so function pointers work).
// In our case we check that &C::serialize has the same signature as the first argument!
// reallyHas<std::string (C::*)(), &C::serialize> should be substituted by
// reallyHas<std::string (C::*)(), std::string (C::*)() &C::serialize> and work!
template <typename U, U u> struct reallyHas;
// Two overloads for yes: one for the signature of a normal method, one is for the signature of a const method.
// We accept a pointer to our helper struct, in order to avoid to instantiate a real instance of this type.
// std::string (C::*)() is function pointer declaration.
template <typename C> static yes& test(reallyHas<std::string (C::*)(), &C::serialize>* /*unused*/) { }
template <typename C> static yes& test(reallyHas<std::string (C::*)() const, &C::serialize>* /*unused*/) { }
// The famous C++ sink-hole.
// Note that sink-hole must be templated too as we are testing test<T>(0).
// If the method serialize isn't available, we will end up in this method.
template <typename> static no& test(...) { /* dark matter */ }
// The constant used as a return value for the test.
// The test is actually done here, thanks to the sizeof compile-time evaluation.
static const bool value = sizeof(test<T>(0)) == sizeof(yes);
};
// Using the struct A, B, C defined in the previous hasSerialize example.
std::cout << hasSerialize<A>::value << std::endl;
std::cout << hasSerialize<B>::value << std::endl;
std::cout << hasSerialize<C>::value << std::endl;
结构体reallyHas使用来保证serialize是一个类成员方法,而不是一个类成员变量或其他东西的。
为了简单,没有考虑仿函数的情况:
struct E
{
struct Functor
{
std::string operator()()
{
return "I am a E!";
}
};
Functor serialize;
};
E e;
std::cout << e.serialize() << std::endl; // Succefully call the functor.
std::cout << testHasSerialize(e) << std::endl; // Output 0.
template <class T> std::string serialize(const T& obj)
{
if (hasSerialize<T>::value) {
return obj.serialize(); // error: no member named 'serialize' in 'A'.
} else {
return to_string(obj);
}
}
A a;
serialize(a);
大功告成!!
编译失败!!??没想到吧,呵呵。
将模板展开后是这样的:
std::string serialize(const A& obj)
{
if (0) { // Dead branching, but the compiler will still consider it!
return obj.serialize(); // error: no member named 'serialize' in 'A'.
} else {
return to_string(obj);
}
}
看出问题了吧。虽然分支0永远也跑不到,但是编译器还是便以这个分支下的代码的。0的分支,说明obj没有serialize函数,但是却调用了,当然出错了。这个问题如何解决呢?答案就是不用if语句了,而是将这个函数分成两个函数,每个函数对应一个分支。如何分?用enable_if:
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.
// Usage:
enable_if<true, int>::type t1; // Compiler happy. t's type is int.
enable_if<hasSerialize<B>::value, int>::type t2; // Compiler happy. t's type is int.
enable_if<false, int>::type t3; // Compiler unhappy. no type named 'type' in 'enable_if<false, int>';
enable_if<hasSerialize<A>::value, int>::type t4; // no type named 'type' in 'enable_if<false, int>';
通过enable_if,我们的函数可以分成下面这样两个:
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);
}
A a;
B b;
C c;
// The following lines work like a charm!
std::cout << serialize(a) << std::endl;
std::cout << serialize(b) << std::endl;
std::cout << serialize(c) << std::endl;
decltype 可以给出一个表达式最终的类型:
B b;
decltype(b.serialize()) test = "test"; // Evaluate b.serialize(), which is typed as std::string.
// Equivalent to std::string test = "test";
declval 主要是为decltype服务的. 有了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';
}
auto相当于c#中的var,有栗子:
bool f();
auto test = f(); // Famous usage, auto deduced that test is a boolean, hurray!
// vvv t wasn't declare at that point, it will be after as a parameter!
template <typename T> decltype(t.serialize()) g(const T& t) { } // Compilation error
// Less famous usage:
// vvv auto delayed the return type specification!
// vvv vvv the return type is specified here and use t!
template <typename T> auto g(const T& t) -> decltype(t.serialize()) { } // No compilation error.
constexpr int factorial(int n)
{
return n <= 1? 1 : (n * factorial(n - 1));
}
int i = factorial(5); // Call to a constexpr function.
// Will be replace by a good compiler by:
// int i = 120;
std::true_type & std::false_type :
struct testStruct : std::true_type { }; // Inherit from the true type.
constexpr bool testVar = testStruct(); // Generate a compile-time testStruct.
bool test = testStruct::value; // Equivalent to: test = true;
test = testVar; // true_type has a constexpr converter operator, equivalent to: test = true;
template <class T> struct hasSerialize
{
// We test if the type has serialize using decltype and declval.
template <typename C> static constexpr decltype(std::declval<C>().serialize(), bool()) test(int /* unused */)
{
// We can return values, thanks to constexpr instead of playing with sizeof.
return true;
}
template <typename C> static constexpr bool test(...)
{
return false;
}
// int is used to give the precedence!
static constexpr bool value = test<T>(int());
};
在 decltype 中,所有的表达式都会被计算,但只有最后的表达式的类型会被认为是最终的类型。在上面的栗子中,前面的表达式仅仅是用来触发Substitution Failure的。
这种方案用到了 std::true_type 和 std::false_type:
// Primary template, inherit from std::false_type.
// ::value will return false.
// Note: the second unused template parameter is set to default as std::string!!!
template <typename T, typename = std::string>
struct hasSerialize
: std::false_type
{
};
// Partial template specialisation, inherit from std::true_type.
// ::value will return true.
template <typename T>
struct hasSerialize<T, decltype(std::declval<T>().serialize())>
: std::true_type
{
};
优先使用最合适的,如果大家都一样合适,那么就使用"most specialized"的。
C++14 中,auto关键字得到了增强,可以用来表示函数或方法的返回类型了:
auto myFunction() // Automagically figures out that myFunction returns ints.
{
return int();
}
如果返回类型编译器猜不出来,就不能用了。
C++11 就有 lambdas 了,语法如下:
[capture-list](params) -> non-mandatory-return-type { ...body... }
C++14 加强了 lambdas,使之可以接受auto类型的参数了。 Lambdas是通过匿名类型来实现的,如果一个 lambda 有 auto类型的参数, 那么它的 "Functor operator"operator() 会实现成一个模版:
// ***** Simple lambda unamed type *****
auto l4 = [](int a, int b) { return a + b; };
std::cout << l4(4, 5) << std::endl; // Output 9.
// Equivalent to:
struct l4UnamedType
{
int operator()(int a, int b) const
{
return a + b;
}
};
l4UnamedType l4Equivalent = l4UnamedType();
std::cout << l4Equivalent(4, 5) << std::endl; // Output 9 too.
// ***** auto parameters lambda unnamed type *****
// b's type is automagically deduced!
auto l5 = [](auto& t) -> decltype(t.serialize()) { return t.serialize(); };
std::cout << l5(b) << std::endl; // Output: I am a B!
std::cout << l5(a) << std::endl; // Error: no member named 'serialize' in 'A'.
// Equivalent to:
struct l5UnamedType
{
template <typename T> auto operator()(T& t) const -> decltype(t.serialize()) // /!\ This signature is nice for a SFINAE!
{
return t.serialize();
}
};
l5UnamedType l5Equivalent = l5UnamedType();
std::cout << l5Equivalent(b) << std::endl; // Output: I am a B!
std::cout << l5Equivalent(a) << std::endl; // Error: no member named 'serialize' in 'A'.
既然有auto参数的lambda用到了模版,那么SFINAE就可以应用到这里了:
// Check if a type has a serialize method.
auto hasSerialize = hana::is_valid([](auto&& x) -> decltype(x.serialize()) { });
hana::is_valid是个函数,这个函数以我们的lambda为参数,返回一个类型。我们给这个返回的类型取名叫容器,这个容器负责将lambda的匿名类型保存起来,方便以后用。我们先看下这个containter的作用:
template <typename UnnamedType> struct container
{
// Remembers UnnamedType.
};
template <typename UnnamedType> constexpr auto is_valid(const UnnamedType& t)
{
// We used auto for the return type: it will be deduced here.
return container<UnnamedType>();
}
auto test = is_valid([](const auto& t) -> decltype(t.serialize()) {})
// Now 'test' remembers the type of the lambda and the signature of its operator()!
接下来我们来实现这个container,这个container需要有一个操作符operator()函数,我们需要通过这个函数来调用我们的lambda,我们的lambda需要一个参数(就是要检验它有没有serialize成员方法),所以容器的operator()也要有一个参数
template <typename UnnamedType> struct container
{
// Let's put the test in private.
private:
// We use std::declval to 'recreate' an object of 'UnnamedType'.
// We use std::declval to also 'recreate' an object of type 'Param'.
// We can use both of these recreated objects to test the validity!
template <typename Param> constexpr auto testValidity(int /* unused */)
-> decltype(std::declval<UnnamedType>()(std::declval<Param>()), std::true_type())
{
// If substitution didn't fail, we can return a true_type.
return std::true_type();
}
template <typename Param> constexpr std::false_type testValidity(...)
{
// Our sink-hole returns a false_type.
return std::false_type();
}
public:
// A public operator() that accept the argument we wish to test onto the UnnamedType.
// Notice that the return type is automatic!
template <typename Param> constexpr auto operator()(const Param& p)
{
// The argument is forwarded to one of the two overloads.
// The SFINAE on the 'true_type' will come into play to dispatch.
// Once again, we use the int for the precedence.
return testValidity<Param>(int());
}
};
template <typename UnnamedType> constexpr auto is_valid(const UnnamedType& t)
{
// We used auto for the return type: it will be deduced here.
return container<UnnamedType>();
}
// Check if a type has a serialize method.
auto hasSerialize = is_valid([](auto&& x) -> decltype(x.serialize()) { });
如果走丢了,建议看下原文。翻译的太渣了(翻译的渣,主要是因为自己还弄不太明白SFINAE)。
看看最后成果吧:
// Notice how I simply swapped the return type on the right?
template <class T> auto serialize(T& obj)
-> typename std::enable_if<decltype(hasSerialize(obj))::value, std::string>::type
{
return obj.serialize();
}
template <class T> auto serialize(T& obj)
-> typename std::enable_if<!decltype(hasSerialize(obj))::value, std::string>::type
{
return to_string(obj);
}
FINALLY!!!
Hey, hey! Don't close this article so fast! If you are true a warrior, you can read the last part!
喂,喂!不要急着走啊!真的猛士是会看原文的!(翻译时,删掉很多不会翻译的,原文后面很有好几段)