想写这篇文章主要是偶然看到很多代码使用protobuf里的反射,例如我们想要获取字段common.logid
对应的value,很多实现使用GetReflection/GetString/field/HasFeild
这些接口来获取。
/*
部分schema定义
message Common {
optional string logid = 1;
}
optional Common common = 1;
*/
const google::protobuf::Descriptor* descriptor = message->GetDescriptor();
const google::protobuf::Reflection* reflection = message->GetReflection();
const google::protobuf::FieldDescriptor* common_field =
descriptor->FindFieldByName("common");
assert(common_field != NULL);
const google::protobuf::Message& common_message = reflection->GetMessage(*message, common_field);
const google::protobuf::Descriptor* common_descriptor = common_message.GetDescriptor();
const google::protobuf::Reflection* common_reflection = common_message.GetReflection();
const google::protobuf::FieldDescriptor* logid_field =
common_descriptor->FindFieldByName("logid");
assert(logid_field != NULL);
return common_reflection->GetString(common_message, logid_field);
虽然pb里的反射实现上性能并没有明显降低,不过一直在思考能否有更简洁的方案实现,比如直接调用logid
这个方法,但是不知道对象类型的情况下,这么写一定会导致编译错误。
而解决的方案就在于SFINAE.
1. SFINAE是什么
SFINAE是C++模板编译的一个原则,全名叫做Substitution Failure Is Not An Error
C++编译过程中,如果模板实例化后的某个模板函数(模板类)对该调用无效,那么将继续寻找其他重载决议,而不是引发一个编译错误。
To summarize, the essence of the SFINAE principle is this: If an invalid argument or return type is formed when a function template is instantiated during overload resolution, the function template instantiation is removed from the overload resolution set and does not result in a compilation error.
说着比较绕口,实际编程中我们或多或少都使用过这个原则,看个例子:
struct Test {
typedef int foo;
};
template <typename T>
//要求类型T定义了内嵌类型foo
void f(typename T::foo) {} // Definition #1
template <typename T>
void f(T) {} // Definition #2
int main() {
f<Test>(10); // Call #1.
f<int>(10); // Call #2. Without error (even though there is no int::foo) thanks to SFINAE.
return 0;
}
模板函数f
一共定义了两个版本。
f<int>
在适配version-1时因为没有nested type: foo
,会引发一个template argument deduction/substitution failed
,编译器不会报错,而是通过version-2实例化出一个f<int>
来。
nm可以看到bin文件里的符号名
00000000004007d1 W void f<Test>(Test::foo)
00000000004007da W void f<int>(int)
2. SFINAE的例子
SFINAE原则最开始被设计出来,是应用于上面C++模板实例化的编译场景。
而后来C++ developers发现SFINAE可以用于编译期决断,特别是配合sizeof使用下,可以方便的判断诸如:类是否定义了内嵌类型,是否定义了给定名字的成员函数等。
2.1. 查看类是否定义了内嵌类型
例如我们想查看类T是否有T::iterator
这个类型
#include <iostream>
#include <vector>
template <typename T>
struct has_typedef_iterator {
// 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[2];
template <typename C>
static yes& test(typename C::iterator*);
template <typename>
static no& test(...);
// If the "sizeof" of the result of calling test<T>(nullptr) is equal to sizeof(yes),
// the first overload worked and T has a nested type named iterator.
static const bool value = sizeof(test<T>(nullptr)) == sizeof(yes);
};
struct foo {
typedef float iterator;
};
int main() {
std::cout << std::boolalpha;
std::cout << has_typedef_iterator<foo>::value << std::endl;//true
std::cout << has_typedef_iterator<int>::value << std::endl;//false
std::cout << has_typedef_iterator<std::vector<int> >::value << std::endl;//true
return 0;
}
如果T定义了内嵌类型(nested type): iterator,例如foo,std::vector<...>
,那么会适配第一个test
模板函数,通过传入nullptr
我们可以省略了构造一个对象指针的过程,返回值为yes
。
如果T未定义iterator,例如int,由于SFINAE原则,适配第一个失败后编译器继续适配第二个并且成功,返回值为no
。
由于yes no
被特意设计了不同的size,配合sizeof,我们就可以判断传入的类型是否定义了iterator
。
注意编译器有一个最优原则,比如foo
同样适配于第二个test
模板,但是第一个test
优先级更高(更加适配),因此编译器选择最优的第一个test
模板,并不会报ambiguous
。
2.2. 现代C++查看类是否定义了内嵌类型
上面的功能,C++11的语法实现上要更简洁一些
#include <iostream>
#include <vector>
#include <type_traits>
template <typename U>
struct iterator_help {
typedef void iterator;
};
template <typename T, typename = void>
struct has_typedef_iterator : std::false_type {};
template <typename T>
struct has_typedef_iterator<T, typename iterator_help<typename T::iterator>::iterator > : std::true_type {};
struct foo {
typedef float iterator;
};
int main() {
std::cout << std::boolalpha;
std::cout << has_typedef_iterator<foo>::value << std::endl;//true
std::cout << has_typedef_iterator<int>::value << std::endl;//false
std::cout << has_typedef_iterator<std::vector<int> >::value << std::endl;//true
return 0;
}
还有一些更加简洁的写法,需要更高级版本的编译器支持,具体可以参考附录里wiki的内容。
2.3. 使用boost查看类是否定义了内嵌类型
boost.mpl提供了BOOST_MPL_HAS_XXX_TRAIT_DEF,可以协助我们方便的测试类型T是否定义了某个内嵌类型。
#include <iostream>
#include <vector>
#include "boost/mpl/has_xxx.hpp"
BOOST_MPL_HAS_XXX_TRAIT_DEF(iterator);
int main() {
std::cout << std::boolalpha;
std::cout << has_iterator<int>::value << std::endl;//false
std::cout << has_iterator<std::vector<int> >::value << std::endl;//true
return 0;
}
2.4 查看类是否定义了某个成员函数
An introduction to C++’s SFINAE concept: compile-time introspection of a class member详细介绍了从c98到c++17的做法
这里介绍下boost里tti的用法 。
#include <vector>
#include <iostream>
#include "boost/tti/has_member_function.hpp"
BOOST_TTI_HAS_MEMBER_FUNCTION(foo);
BOOST_TTI_HAS_MEMBER_FUNCTION(bar);
struct Foo {
void foo();
};//foo
struct Bar {
void bar();
};//Bar
int main() {
std::cout << std::boolalpha
<< has_member_function_foo<Foo, void>::value << std::endl
<< has_member_function_bar<Bar, void>::value << std::endl;
return 0;
}
3. enable_if的作用
enable_if
的出现使得SFINAE使用上更加方便,进一步扩展了上面has_xxx is_xxx
的作用。
而enable_if
实现上也是使用了SFINAE。
boost与std里都有定义,接下来的例子可能都有用到。
3.1. cppreference的例子
首先从cppreference的例子看下enable_if的两种用法
// enable_if example: two ways of using enable_if
#include <iostream>
#include <type_traits>
// 1. the return type (bool) is only valid if T is an integral type:
template <class T>
typename std::enable_if<std::is_integral<T>::value,bool>::type
is_odd (T i) {return bool(i%2);}
// 2. the second template argument is only valid if T is an integral type:
template < class T,
class = typename std::enable_if<std::is_integral<T>::value>::type>
bool is_even (T i) {return !bool(i%2);}
int main() {
short int i = 1; // code does not compile if type of i is not integral
std::cout << std::boolalpha;
std::cout << "i is odd: " << is_odd(i) << std::endl;
std::cout << "i is even: " << is_even(i) << std::endl;
return 0;
}
上面代表了enable_if的两种惯用方法:
- 返回值类型使用enable_if
- 模板参数额外指定一个默认的参数class = typename std::enable_if<…>::type
推荐使用第一种,更方便些。
使用enable_if的好处是控制函数只接受某些类型的(value==true)的参数,否则编译报错。
比如如果我们增加这么一句
std::cout << "i is even: " << is_even(100.0) << std::endl;
编译失败提示:
test_enable_if.cpp: In function ‘int main()’:
test_enable_if.cpp:37:46: error: no matching function for call to ‘is_even(double)’
std::cout << "i is even: " << is_even(100.0) << std::endl;
^
test_enable_if.cpp:37:46: note: candidate is:
test_enable_if.cpp:28:6: note: template<class T, class> bool is_even(T)
bool is_even (T i) {return !bool(i%2);}
^
test_enable_if.cpp:28:6: note: template argument deduction/substitution failed:
test_enable_if.cpp:27:12: error: no type named ‘type’ in ‘struct std::enable_if<false, void>’
class = typename std::enable_if<std::is_integral<T>::value>::type>
注意这么一句是关键:
error: no type named ‘type’ in ‘struct std::enable_if<false, void>’
接下来我们看下enable_if
的实现,就能找到这句编译失败提示的来源。
3.2. enable_if的实现
C++模板元编程里的定义
template <bool, typename T=void>
struct enable_if {
};
template <typename T>
struct enable_if<true, T> {
using type = T;
};
可以看到当enable_if第一个类型为true时会特化到第二种实现,此时内嵌类型type存在。
否则编译器匹配第一种实现,内嵌类型type不存在,这也是上面编译操作提示的原因。
boost里的实现位于core/enable_if.cpp
namespace boost
{
template <bool B, class T = void>
struct enable_if_c {
typedef T type;
};
template <class T>
//false时不存在内嵌类型type
struct enable_if_c<false, T> {};
template <class Cond, class T = void>
//::value是type_traits的常用静态变量定义
struct enable_if : public enable_if_c<Cond::value, T> {};
template <bool B, class T>
struct lazy_enable_if_c {
typedef typename T::type type;
};
template <class T>
struct lazy_enable_if_c<false, T> {};
template <class Cond, class T>
struct lazy_enable_if : public lazy_enable_if_c<Cond::value, T> {};
//disable与enbale相反,true时不存在type类型
template <bool B, class T = void>
struct disable_if_c {
typedef T type;
};
template <class T>
struct disable_if_c<true, T> {};
template <class Cond, class T = void>
struct disable_if : public disable_if_c<Cond::value, T> {};
template <bool B, class T>
struct lazy_disable_if_c {
typedef typename T::type type;
};
template <class T>
struct lazy_disable_if_c<true, T> {};
template <class Cond, class T>
struct lazy_disable_if : public lazy_disable_if_c<Cond::value, T> {};
} // namespace boost
3.3. enable_if的使用
借助于boost的众多type_traits接口,可以很方便的决定:
将/不将某种类型加入到我们模板实例化决议集合里。
#include <boost/utility/enable_if.hpp>
#include <type_traits>
#include <string>
#include <iostream>
template <typename T>
void print(typename boost::enable_if<std::is_integral<T>, T>::type i) {
std::cout << "Integral: " << i << '\n';
}
template <typename T>
void print(typename boost::enable_if<std::is_floating_point<T>, T>::type f) {
std::cout << "Floating point: " << f << '\n';
}
int main()
{
print<short>(1);
print<long>(2);
print<double>(3.14);
return 0;
}
boost里type_traits还有例如is_class has_trival_copy has_virtual_detructor
等,具体可以参考boost官方文档type_traits
一节
4. 只在参数类有指定成员函数名定义的情况下调用该成员函数
回到最开始的想法,假设我们希望只有传入的对象,有logid
这个成员函数时就调用logid,应该怎么做?
#include <iostream>
#include "boost/tti/has_member_function.hpp"
//BOOST_TTI_HAS_MEMBER_FUNCTION宏会生成has_member_function_logid函数
BOOST_TTI_HAS_MEMBER_FUNCTION(logid);
//foo zoo定义了void logid()函数
struct foo {
void logid() { std::cout << "foo" << std::endl; }
};//foo
struct bar {
};//bar
struct zoo {
void logid() { std::cout << "zoo" << std::endl; }
};//zoo
//第二个参数通过enable_if指定只有存在成员函数logid下才有效
template <class T, class = typename std::enable_if<has_member_function_logid<T, void>::value>::type>
void dohana(T t) {
t.logid();
}
int main() {
//测试has_member_function函数
std::cout << has_member_function_logid<foo, void>::value << std::endl;//true
std::cout << has_member_function_logid<bar, void>::value << std::endl;//false
std::cout << has_member_function_logid<zoo, void>::value << std::endl;//true
std::cout << has_member_function_logid<zoo, int>::value << std::endl;//false
dohana(foo());//foo
dohana(zoo());//zoo
//compiler error:error: no type named ‘type’ in ‘struct std::enable_if<false, void>’
// dohana(bar());
return 0;
}
这样我们就实现了在编译期间调用指定函数,而不是运行期间通过反射来做。
5. 更多应用
实际上SFINAE在大型项目里非常常见,例如protobuf有大量使用:
...
template <typename I, typename = typename std::enable_if<
std::is_integral<I>::value>::type>
static std::string ToString(I x) {
return StrCat(x);
}