注1:本文至少需要编译器支持C 11。
注2:本文不考虑使用宏。
一、老办法
在写C 的时候,有时候可能需要检查一个类是否有特定的成员类型,例如:
// 检查 T::type 是否存在,存在则 value 为 true,否则为 falsetemplate <typename T>struct has_type;
struct A {};struct B { using type = int; };static_assert(!has_type::value, "Failed"); // 不存在static_assert(has_type::value, "Failed"); // 存在
或者需要检查一个类是否有特定的成员函数,例如:
// 检查 T.get() 是否存在,存在则 value 为 true,否则为 falsetemplate <typename T>struct has_get;
struct A {};struct B { int get() { return 42; } };static_assert(!has_get::value, "Failed"); // 不存在static_assert(has_get::value, "Failed"); // 存在
如果是在很久以前的 C 98 时代,可能会这样利用 SFINAE 实现:
template struct has_type {private: typedef char one; typedef struct { char data[2]; } two; // 存在的话返回类型为 one template static one test(typename U::type*); // 不存在的话返回类型为 two template static two test(...);public: enum { value = sizeof(test(0)) == sizeof(one) };};
如果 T::type 存在的话就会选择第一个重载,否则就会选择第二个重载,由此判断 T::type 是否存在。但是这样的代码阅读起来可能会挺费劲的……于是,现在有了 void_t!
二、void_t
void_t<...> 其实就是 void,但它可以在 SFINAE 中帮助判断类型是否存在,示例如下:
template <typename T, typename = void>struct has_type : std::false_type {};template struct has_type<T, void_t> : std::true_type {};
(看起来是不是和 enable_if 的某种用法有相似之处?)
虽然 void_t 在 C 17 才成为标准库的一部分,但是我们可以在 C 11 中自己造一个:
template struct make_void { using type = void; };template using void_t = typename make_void::type;
需要注意的是上面的定义是为了兼容 C 11 / C 14 而这样写的,因为别名模板中未被使用的模板参数可能会被忽略。但如果是 C 17 的话,编译器就不能忽略别名模板中未被使用的模板参数,就可以直接这样写:
template using void_t = void;
(当然有 C 17 的话就能用标准库的 std::void_t 了……)
同理,我们也可以用同样的方式判断成员函数是否存在:
template <typename T, typename = void>struct has_get : std::false_type {};template struct has_get<T, void_t<decltype(std::declval().get())>> : std::true_type {};
三、Detection Idiom:is_detected
即使我们有了 void_t,但每次需要一个新的判定就得再撸一遍 SFINAE,依然有点不够直观(你说用宏?…… 我什么都没听到)。那么我们为什么不把这种判定也提炼成模板呢?有请 is_detected 出场——
template using has_type_t = typename T::type;template using has_type = is_detected;
看起来使用 is_detected 的方法比之前的 has_type 清爽多了吧,而且非常直观。
虽然 is_detected 还没有进入标准,但我们依然可以在 C 11 中把它造出来:
template <typename, template <typename...> class Op, typename... T>struct is_detected_impl : std::false_type {};template <template <typename...> class Op, typename... T>struct is_detected_impl<void_t<Op>, Op, T...> : std::true_type {};
template <template <typename...> class Op, typename... T>using is_detected = is_detected_impl;
如果仔细看的话,你就能够发现这就是给之前的方法加上了模板模板参数,使得它更容易使用。下面是用 is_detected 判断成员函数是否存在:
template using has_get_t = decltype(std::declval().get());template using has_get = is_detected;
当然,is_detected 还可以做到更多,只要你能够写出 Op 的话就有很多可以做的事情,比如说做各种 concept 的检查。除了 is_detected 之外,Detection Idiom 还有 detected_t 和 detected_or 等工具,可以用于在 trait 中实现默认类型,这里就不再展开介绍,感兴趣的话可以到上面的链接里看一下。
来源:https://zhuanlan.zhihu.com/p/2615546