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