最近写多了 Rust,觉得 trait 特别香,所以写 C++ 的时候也特别想用上 concept
这个基本等价的特性,用于检查模板参数类型是否实现了某个特定的函数。然而由于某些原因,项目只用上了 C++ 17。经过艰难的摸(xia)索(xie),终于研究出了一种基于 SFINAE 的方法来实现这个需求。下面直接放代码,以及一些简单的解释。在这里非常感谢 yjp 给我的极大帮助,还有和我一起浪费的时间。
更新:看到 TS 中有一个特性叫 is_detected
,看来就是我需要的。考虑到 TS 猴年马月才能用上,这个轮子也不算没用。
土制实现
Naive Way
首先,我们有一种非常直观的实现。如果要检查某个类是否有方法 foo
,可以这样写:
template <typename T> struct has_member_func_foo {
template <typename C> static std::true_type test(decltype(&C::foo));
template <typename C> 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 <typename T, typename R> struct has_generic_member_func_foo {
template <typename C> static std::true_type test(decltype(&C::template foo<R>));
template <typename C> 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 <typename T, typename C, typename ... Args> struct has_any_generic_member_func_foo;
template <typename T, typename C> struct has_any_generic_member_func_foo <T, C> {
static constexpr bool value = has_generic_member_func_foo<T, C>::value;
};
template <typename T, typename C, 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)
等即可。同样,允许多参数的泛型成员函数也没有简单的实现。
测试
使用下面的错误代码(缺失实现):
struct A {};
HAS_MEMBER_FUNC_CHECKER(foo);
HAS_GENERIC_MEMBER_FUNC_CHECKER(bar);
HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECK(baz);
int main() {
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);
return 0;
}
此时,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"
struct A {
template <typename T> void baz(T input) {
input.hello();
}
};
struct has_hello { void hello() {} };
struct no_hello {};
HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECK(baz);
int main() {
static_assert(HAS_MULTIPLE_GENERIC_MEMBER_FUNC_CHECKER_NAME(baz)<A, no_hello, has_hello>::value);
return 0;
}
理论上应该能通过编译,对此 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++ 好难啊,我好菜啊。