对2015.05技术沙龙中江南群主“任性”故事的探讨

五月份技术沙龙第二场演讲中,江南群主讲述的第二个“任性”故事的一个细节引发了一些争议。经小生仔细观察,三位讲师都有使用到相同的技术,但是实现的方法都有所不同,本文特来对这一个问题做一次详细的研究。

众所周知,在C++中实现反射是一件很棘手的事情。但是在确保没有类型退化(decay)和没有隐式类型转换的干扰下,是用模板元编程的一些技巧可以帮助我们完成一些目标。比如判断某个类型中是否含有某个成员函数,本文中的类成员函数仅指非静态成员函数

现实的基本思想是“一瞥”( catch a glimps of )这个成员函数。一瞥实际上是编译期的某种计算,而不会生成实际的代码。例如非求值运算符( Unevaluated Operator ),重载解析模板特化。非求值运算符在C++98下只有sizeof,到了C++11标准又扩充了decltype,typeid和noexcept三个操作符。先来看看C++98的实现。

由于sizeof只能对类型大小求值,因此C++98的版本的基本思想就是利用SFINAE和重载决议,配合sizeof来计算函数返回类型的大小,而获知哪一个test函数是决议的结果。如果是返回One类型的test,那么可以判断出这个类型T含有命名为foo,返回类型为void且有一个形参为void的成员函数。

这里有两个问题。1. 是否含有成员函数foo,单单只知道成员函数名是不够的,还必须知道它的完整签名,如返回值(暂不考虑cv修饰符)。2. 重载解析的规则里面有讲,相同的形参和不同返回类型的重载函数是有歧义的。因此,我们只需要知道形参就足够了,而不需要知道其完整的签名。

问题一很好解决,就是啰嗦点,除了提供函数名称之外,所有的形参和返回值类型一并提供。在c++98中,想做到这一点更啰嗦。我们需要提供形参数目从1到N每种版本的实现,具体实现多少个N由类库作者自行估量。

第二个问题,我们要寻求其他的实现方法了。因为指向非静态成员函数的指针(pointer to non-static memeber function)的签名有返回值类型,还有cv修饰符。基本思路是,假装我们要调用这个成员函数。先看看实现。

sizeof(((T*)0)->foo()),这一句就是假装去调用这个函数并尝试去计算函数返回值的类型的大小。由于sizeof是非求值运算符,因此没有生成运行期的空指针调用foo的代码。从这里就能看出sizeof操作符已经心有余而力不足了。首先sizeof将类型映射到了类型大小,丢失了类型信息;其次sizeof无法对void求值,这也是这个实现版本的硬伤。总之小生受能力所限,一直无法在cpp98版本中寻求比较好的实现方法。拜求得道大神之不吝赐教。

到了c++11的时代,情况好了许多。decltype这个非求值运算符的出现,让C++98中实现has_member_foo的第二种思路得以实现。先看看实现代码。

基本思想同样是SFINAE和重载解析,配合非求值运算符。只不过换成了delctype运算符。相比于sizeof运算符,decltype保留了非求值表达返回类型的完整信息,更重要的是decltype也能对void计算,不会出现sizeof(void)的尴尬。

深入细节,我们来看看C++11的实现版本是如何一瞥foo的调用的。通常,我们在描述数学问题的时候,总是使用假设。这里类比之,“假设”有一个类型的实例,且“假设”有可列的实参的全部实例,“尝试”使用这些实例调用函数foo,让decltype演绎这次假设的返回值结果!由于decltype是非求值运算符,所以计算中对象不需要真正的实例,同样函数只需要声明而不需要函数体。std::declval就是这样的一个函数。在std命名空间中就是返一个universal reference,并没有函数体。std::decltype只能用于非求值的计算当中。小生认为这种实现是最为简单的,如果使用指向非静态成员函数的指针来实现还要考虑返回值和cv修饰符,而这一种是完全不用考虑的。

接下来扩展一下思路。

拓展思路-另外的实现方法。文章的开头提及到的非求值的编译期计算,除了非求值运算符和重载解析外,还有一个模板特化。那么是否可以通过模板特化来实现本文的需求呢?先来看看基本思路。has_member_foo的泛型实现,应当返回false,继承std::false_type即可。在某种情况下的特化继承std::true_type!听起来是不是很美好?!实现细节。首先,为了简化问题,先不考虑成员函数有形参的情况。元函数has_member_foo的第一个参数是T,要判断的类型,应该是雷打不动的。而我们需要额外的一个参数,为特化的实现所用。

先举一个例子。还记得元函数enable_if配合模板特化的技巧吗?元函数template <typename T> struct size_integral;只计算整形类型的大小,其他类型不做任何计算。看看实现的代码。

当编译器尝试使用某个类型T去实例化size_integral的时候,会实例化所有的特化版本,并决议最佳的匹配。如果T是一个整形,那么特化版本会选择;如果T不是整形,那么特化版本实例化替换失败,根据SFINAE,会匹配泛型的实例化版本。

现在的问题是,针对本文的需求,我们需要一个元函数工具,使得传递任意可列个C++参数类型给它,它始终导出一个void. 为了迎合上面给出的例子的形式,小生才选择void。于是我们构建一个这样的工具,其实现如下。

也有了本文需求的另外一个实现

如果decltype能够成功推导出voider_t中的类型,那么决议就会选择这个more specialized的版本;反之,就会选择泛化的版本。实现是不是更优雅了?使用模板偏特化减少了使用重载解析造成的额外代码,至少小生谨以为然。

只可惜目前的编译器都不支持这种实现,原因是因为voider元函数没有使用任何模板参数,而被编译期做了优化。C++委员会也有一个提案来解决这个问题,详见CWG 1558, treatment of unused arguments in an alias template specialization. 其中的原委,小生还不是很清楚,望闻得道大神指教。

拓展思路-易用性。江南群主在这个故事中也提到了,这个元函数只能针对某一个成员函数,换一个成员函数要重新实现一个,中间有重复代码。因此有了使用宏来生成代码的实现版本,详见江南群主ppt. 可是江南群主的版本必须“两步走”!先得使用宏声明,再使用。有没有什么方法可以使用一个宏就能搞定?经过小生的测试,仅在msvc平台下还有一个小坑,把宏写在函数体内似乎有点问题,始终都找不到这个模板,不知道其他平台如何。因此一定要把宏写在函数体以外。此外,还需要考虑命名空间的问题。其实易用性的实现就目前的C++标准还是有些不够。

鄙文先写到这里。对于C++1y技术感兴趣的同学请加入我们社区,加入QQ群296561497讨论,让C++的世界更美好。

《对2015.05技术沙龙中江南群主“任性”故事的探讨》有2个想法

  1. 这个voider可以用来判断是否存在某种类型,会很方便的。
    可以这样写:
    template<typename... T>struct void_t{ using type = void; };
    #define HAS_TYPE(token)\
    template<typename T, typename = void>\
    struct has_member_##token : std::false_type{}; \
    template<typename T>\
    struct has_member_##token<T, typename void_t<typename T::token>::type> : std::true_type{}; \

    HAS_TYPE(type)

    void test(){    cout<<has_member_type<int>::value<<endl;//0}

发表评论

Copy Protected by Chetan's WP-Copyprotect.