美文网首页
土制 concept (使用模板检测类成员函数终极版)

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

作者: Aska偶阵雨 | 来源:发表于2019-12-20 16:40 被阅读0次

    原文地址: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++ 好难啊,我好菜啊。

    相关文章

      网友评论

          本文标题:土制 concept (使用模板检测类成员函数终极版)

          本文链接:https://www.haomeiwen.com/subject/rueinctx.html