土制 concept (使用模板检测类成员函数终极版)

2019-12-20  本文已影响0人  Aska偶阵雨

原文地址:https://harrychen.xyz/2019/06/04/cpp-17-mock-concept/?nsukey=gtNT%2F95OLtBrNsgLAsZOVMljsbaBpJ7REBnA1BvfikjM9tyPnENnFyjpsh%2BhRwlALgEl4BCk73Zz3o5nPVLCWQhoMlWgL8h1GPiQJ%2BS%2FrpVE6IBF8gG4s9La1WsVI%2BPGPmZoVO8iT54Lr%2Bc4H6dtqZYJ1Q7tZBtkeWTLdA%2Fe9bVIAgatgmL2ZDfocdgXQRBPRBvoJd2IgJpM9X1q51HKZQ%3D%3D&from=timeline&isappinstalled=0

最近写多了 Rust,觉得 trait 特别香,所以写 C++ 的时候也特别想用上concept这个基本等价的特性,用于检查模板参数类型是否实现了某个特定的函数。然而由于某些原因,项目只用上了 C++ 17。经过艰难的摸(xia)索(xie),终于研究出了一种基于 SFINAE 的方法来实现这个需求。下面直接放代码,以及一些简单的解释。在这里非常感谢 yjp 给我的极大帮助,还有和我一起浪费的时间

更新:看到 TS 中有一个特性叫is_detected,看来就是我需要的。考虑到 TS 猴年马月才能用上,这个轮子也不算没用。

土制实现

Naive Way

首先,我们有一种非常直观的实现。如果要检查某个类是否有方法foo,可以这样写:

template<typenameT> struct has_member_func_foo

{

template<typenameC> static std::true_type test(decltype(&C::foo));

template<typenameC> static std::false_type test(...);

static constexpr bool value=std::is_same<decltype(test<T>(nullptr)),std::true_type>::value;

};

这是非常典型的 SFINAE,就不多解释了。通过has_member_func_foo<T>::value,就能得到对应的布尔值表示T中是否有foo的实现。这个值可以用于static_assert等场合,在编译期就可以进行判断。

然后呢?

简单的尝试就能知道,上面这种方法没法用在泛型方法上,因为签名并没有办法推导出来。没有关系,再加一个参数:

template<typenameT,typenameR> struct has_generic_member_func_foo

{

template<typenameC> static std::true_type test(decltype(&C::templatefoo<R>));

template<typenameC> static std::false_type test(...);

static constexpr bool value=std::is_same<decltype(test<T>(nullptr)),std::true_type>::value;

};

需要注意这里出现了C::template foo<R>的用法,用于显式告知编译器foo是一个template dependent name(因为并不能从语义上区分),否则会产生编译错误。同样,可以用has_generic_member_func_foo<T, R>::value获得值。

老师,能不能再给力一点?

通常我们会遇到比较复杂的情况,如某个泛型方法对于某几个类型之一进行了实现(即可以通过编译),我们就认为约束被满足了。此时大家往往会想这样写:

static_assert(has_generic_member_func_foo<T,A>::value||has_generic_member_func_foo<T,B>::value);

但这往往不可行,因为虽然求值是懒惰的,但是模板展开不是。只要在任何类型的展开中产生了编译错误,编译器就会报错而停止,这并不是预期的行为。怎么正确地利用 SFINAE 来忽略掉那些错误呢?我们可以使用type_traits中的一些类型与运算来解决这一问题:

template<typenameT,typenameC,typename...Args> struct has_any_generic_member_func_foo;

template<typenameT,typenameC> structhas_any_generic_member_func_foo<T,C>

{

static constexpr bool value=has_generic_member_func_foo<T,C>::value;

};

template<typenameT,typenameC,typename...Args> struct has_any_generic_member_func_foo{

static constexpr bool value=std::disjunction_v<has_any_generic_member_func_foo<T,C>,has_any_generic_member_func_foo<T,Args...>>;

};

其中,std::disjunction_v是对一个std::disjunction<B1, ..., BN>类型取value成员。cppreference中对其解释为:

如果sizeof...(B) == 0,则返回std::false_type

否则返回B1, ..., BN中第一个使得bool(Bi::value) == true的类型,如果没有则返回BN

可以看到,上面代码中的第一个定义为递归基,第二个定义使用std::disjunction递归地进行逻辑或运算。这样,就利用了has_generic_member_func_foo的 SFINAE 特性。

此时,我们用has_any_generic_member_func_foo<T, A, B, C>::value就能够正确地得到结果。

通用实现

上面的实现虽然看起来很科学,但是有一个致命的问题:其中的foo没有办法用变(不能为模板参数)。这从模板代码方面并不能解决,但是 C/C++ 另一个伟大的发明——预处理器,此时就派上了用场。我们可以使用宏来解决通用性问题,只需要为每个函数定义一套类型即可。

首先需要一些字符串拼接的魔法,这里不加以过多解释:

// token concatenation#define STR(S) #S

#define _CAT(A, B) A##B

#define CAT(A, B) _CAT(A, B)

#define CHECKER_PREFIX __has_member_function_

对于非泛型成员函数,实现比较简单:

#define HAS_MEMBER_FUNC_CHECKER_NAME(FUNC) CAT(CHECKER_PREFIX, FUNC)#define HAS_MEMBER_FUNC_CHECKER(FUNC) template <typename T> \

struct HAS_MEMBER_FUNC_CHECKER_NAME(FUNC) \

{ \

    template <typename C> static std::true_type test(decltype(&C::FUNC)); \

    template <typename C> static std::false_type test(...); \

    static constexpr bool value = std::is_same<decltype(test<T>(nullptr)), std::true_type>::value; \

};#define HAS_MEMBER_FUNC(TYPE, FUNC) (HAS_MEMBER_FUNC_CHECKER_NAME(FUNC)<TYPE>::value)

对于一个函数名foo,只要在(非 block scope)中进行一次声明HAS_MEMBER_FUNC_CHECKER(foo),就可以对于任意类型使用HAS_MEMBER_FUNC(T, foo)来进行判断。

对于确定参数类型的泛型成员函数,也是类似的:

#define HAS_GENERIC_MEMBER_FUNC_CHECKER_NAME(FUNC) CAT(HAS_MEMBER_FUNC_CHECKER_NAME(FUNC), __generic)#define HAS_GENERIC_MEMBER_FUNC_CHECKER(FUNC) template <typename T, typename R> \

struct HAS_GENERIC_MEMBER_FUNC_CHECKER_NAME(FUNC) \

{ \

    template <typename C> static std::true_type test(decltype(&C::template FUNC<R>)); \

    template <typename C> static std::false_type test(...); \

    static constexpr bool value = std::is_same<decltype(test<T>(nullptr)), std::true_type>::value; \

};#define HAS_GENERIC_MEMBER_FUNC(TYPE, FUNC, ARG) (HAS_GENERIC_MEMBER_FUNC_CHECKER_NAME(FUNC)<TYPE, ARG>::value)

这里多了一个ARG宏参数,意义是不言自明的。注意这里一个隐藏的坑是其中不能有逗号,否则会被预处理器误解。解决方案是直接使用HAS_GENERIC_MEMBER_FUNC_CHECKER_NAME(foo)<T, ARG>::value来得到值。

对于可以有多个参数类型的泛型成员函数,同样可以定义:

#define HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(FUNC) CAT(HAS_GENERIC_MEMBER_FUNC_CHECKER_NAME(FUNC), __multiple)#define HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER(FUNC) \

HAS_GENERIC_MEMBER_FUNC_CHECKER(FUNC); \

template <typename T, typename T1, typename ... Args> struct HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(FUNC); \

template <typename T, typename T1> struct HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(FUNC) <T, T1> \

{ \

    static constexpr bool value = HAS_GENERIC_MEMBER_FUNC(T, FUNC, T1); \

}; \

template <typename T, typename T1, typename ... Args> struct HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(FUNC) \

{ \

    static constexpr bool value = std::disjunction_v<HAS_GENERIC_MEMBER_FUNC_CHECKER_NAME(FUNC)<T, T1>, \

        HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(FUNC)<T, Args...>>; \

};

这里用到了上面的HAS_GENERIC_MEMBER_FUNC_CHECKER宏。并且此时无法使用宏来直接求值,因为预处理器的__VA_ARGS__参数包会破坏模板参数的结构,导致代码语法错误。同样使用HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(foo)<T, A, B, C>::value即可求值。

友好提示

为了让static_assert在失败时能够有更友好的提示,我们可以增加两个宏:

#define ASSERT_HAS_MEMBER_FUNC(TYPE, FUNC) static_assert(HAS_MEMBER_FUNC(TYPE, FUNC), \

    NO_MEMBER_FUNC_ERROR_MESSAGE(TYPE, FUNC));

#define ASSERT_HAS_GENERIC_MEMBER_FUNC(TYPE, FUNC, ARG) static_assert(HAS_GENERIC_MEMBER_FUNC(TYPE, FUNC, ARG), \

    NO_MEMBER_FUNC_ERROR_MESSAGE(TYPE, FUNC) "<" STR(ARG) ">");

直接在代码中使用ASSERT_HAS_MEMBER_FUNC(T, foo)等即可。同样,允许多参数的泛型成员函数也没有简单的实现。

测试

使用下面的错误代码(缺失实现):

structA{};HAS_MEMBER_FUNC_CHECKER(foo);HAS_GENERIC_MEMBER_FUNC_CHECKER(bar);HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECK(baz);intmain(){ASSERT_HAS_MEMBER_FUNC(A,foo);ASSERT_HAS_GENERIC_MEMBER_FUNC(A,bar,int);static_assert(HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(baz)<A,int,double>::value);return0;}

此时,clang++会报错下面的错误:

member_test.cpp:10:2: error: static_assert failed due to requirement '__has_member_function_foo<A>::value' "Type A does not implement function foo"

        ASSERT_HAS_MEMBER_FUNC(A, foo);

        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

./member_test.hpp:22:44: note: expanded from macro 'ASSERT_HAS_MEMBER_FUNC'

#define ASSERT_HAS_MEMBER_FUNC(TYPE, FUNC) static_assert(HAS_MEMBER_FUNC(TYPE, FUNC), \

                                          ^            ~~~~~~~~~~~~~~~~~~~~~~~~~~~

member_test.cpp:11:2: error: static_assert failed due to requirement '__has_member_function_bar__generic<A, int>::value' "Type A does not implement function bar<int>"

        ASSERT_HAS_GENERIC_MEMBER_FUNC(A, bar, int);

        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

./member_test.hpp:37:57: note: expanded from macro 'ASSERT_HAS_GENERIC_MEMBER_FUNC'

#define ASSERT_HAS_GENERIC_MEMBER_FUNC(TYPE, FUNC, ARG) static_assert(HAS_GENERIC_MEMBER_FUNC(TYPE, FUNC, ARG), \

                                                        ^            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

member_test.cpp:12:2: error: static_assert failed due to requirement '__has_member_function_baz__generic__multiple<A, int, double>::value'

        static_assert(HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(baz)<A, int, double>::value);

        ^            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

3 errors generated.

而g++会报下面的错误:

In file included from member_test.cpp:1:0:

member_test.cpp: In function ‘int main()’:

member_test.hpp:22:44: error: static assertion failed: Type A does not implement function foo

#define ASSERT_HAS_MEMBER_FUNC(TYPE, FUNC) static_assert(HAS_MEMBER_FUNC(TYPE, FUNC), \

                                            ^

member_test.cpp:10:2: note: in expansion of macro ‘ASSERT_HAS_MEMBER_FUNC’

  ASSERT_HAS_MEMBER_FUNC(A, foo);

  ^~~~~~~~~~~~~~~~~~~~~~

member_test.hpp:37:57: error: static assertion failed: Type A does not implement function bar<int>

#define ASSERT_HAS_GENERIC_MEMBER_FUNC(TYPE, FUNC, ARG) static_assert(HAS_GENERIC_MEMBER_FUNC(TYPE, FUNC, ARG), \

                                                        ^

member_test.cpp:11:2: note: in expansion of macro ‘ASSERT_HAS_GENERIC_MEMBER_FUNC’

  ASSERT_HAS_GENERIC_MEMBER_FUNC(A, bar, int);

  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

member_test.cpp:12:2: error: static assertion failed

  static_assert(HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(baz)<A, int, double>::value);

  ^~~~~~~~~~~~~

可以看出报错信息都是很友好的,并直接指出了问题。

一盆冷水

然而我忽然发现,对于多版本的泛型函数判断,当函数没有缺失时似乎有一些问题。具体来说,编译器对于展开函数时的错误处理方法不同。对于下列代码:

#include "member_test.hpp"structA{template<typenameT>voidbaz(Tinput){input.hello();}};structhas_hello{voidhello(){}};structno_hello{};HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECK(baz);intmain(){static_assert(HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(baz)<A,no_hello,has_hello>::value);return0;}

理论上应该能通过编译,对此clang++没有报错,但g++指出了错误:

member_test.cpp: In instantiation of ‘void A::baz(T) [with T = no_hello]’:

member_test.cpp:12:1:  required by substitution of ‘template<class C> static std::true_type __has_member_function_baz__generic<A, no_hello>::test<C>(decltype (& C:: baz<no_hello>)) [with C = A]’

member_test.cpp:12:1:  required from ‘constexpr const bool __has_member_function_baz__generic<A, no_hello>::value’

/usr/include/c++/7/type_traits:120:12:  required from ‘struct std::__or_<__has_member_function_baz__generic<A, no_hello>, __has_member_function_baz__generic__multiple<A, has_hello> >’

/usr/include/c++/7/type_traits:167:12:  required from ‘struct std::disjunction<__has_member_function_baz__generic<A, no_hello>, __has_member_function_baz__generic__multiple<A, has_hello>  ’

/usr/include/c++/7/type_traits:180:27:  required from ‘constexpr const bool std::disjunction_v<__has_member_function_baz__generic<A, no_hello>, __has_member_function_baz__generic__multiple<A, has_hello> >’

member_test.cpp:12:1:  required from ‘constexpr const bool __has_member_function_baz__generic__multiple<A, no_hello, has_hello>::value’

member_test.cpp:15:92:  required from here

member_test.cpp:5:9: error: ‘struct no_hello’ has no member named ‘hello’; did you mean ‘no_hello’?

  input.hello();

  ~~~~~~^~~~~

  no_hello

也就是说,g++在进行static_assert时完全展开了函数,遇到编译错误后直接停止(而不是尝试展开第二个实现)。这显然不是我们想要的。

这是不是说clang++实现得更好呢?并不是,事实上只要有同名的模板成员函数存在,无论实现对于给出的类型是否合法都不会出错,说明clang++可能根本没有尝试展开代码。这显然更不是我们想要的行为。经过测试,MSVC 的行为和clang++也是一致的。

这就给我的瞎搞行为完全判了死刑。

总结

C++ 好难啊,我好菜啊。

上一篇 下一篇

猜你喜欢

热点阅读