C++ 土制 concept

 

最近写多了 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++ 好难啊,我好菜啊。