查看原文
其他

如何优雅地检测类型/表达式有效性?

CPP开发者 2021-07-20

(给CPP开发者加星标,提升C/C++技能)

来源:SuperSodaSea
https://zhuanlan.zhihu.com/p/26155469

【导读】:本文主要讲解如何在模板中检测类型和表达式的有效性。


以下是正文



注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<A>::value, "Failed"); // 不存在static_assert(has_type<B>::value, "Failed"); // 存在

或者需要检查一个类是否有特定的成员函数,例如:

// 检查 T.get() 是否存在,存在则 value 为 true,否则为 falsetemplate <typename T>struct has_get;
struct A {};struct B { int get() { return 42; } };static_assert(!has_get<A>::value, "Failed"); // 不存在static_assert(has_get<B>::value, "Failed"); // 存在

如果是在很久以前的 C++98 时代,可能会这样利用 SFINAE 实现:

template <typename T>struct has_type {private: typedef char one; typedef struct { char data[2]; } two; // 存在的话返回类型为 one template <typename U> static one test(typename U::type*); // 不存在的话返回类型为 two template <typename U> static two test(...);public: enum { value = sizeof(test<T>(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 <typename T>struct has_type<T, void_t<typename T::type>> : std::true_type {};

(看起来是不是和 enable_if 的某种用法有相似之处?)


虽然 void_t 在 C++17 才成为标准库的一部分,但是我们可以在 C++11 中自己造一个:

template <typename... T> struct make_void { using type = void; };template <typename... T> using void_t = typename make_void<T...>::type;

需要注意的是上面的定义是为了兼容 C++11 / C++14 而这样写的,因为别名模板中未被使用的模板参数可能会被忽略。但如果是 C++17 的话,编译器就不能忽略别名模板中未被使用的模板参数,就可以直接这样写:

template <typename...>using void_t = void;

(当然有 C++17 的话就能用标准库的 std::void_t 了……)


同理,我们也可以用同样的方式判断成员函数是否存在:

template <typename T, typename = void>struct has_get : std::false_type {};template <typename T>struct has_get<T, void_t<decltype(std::declval<T&>().get())>> : std::true_type {};

三、Detection Idiom:is_detected


即使我们有了 void_t,但每次需要一个新的判定就得再撸一遍 SFINAE,依然有点不够直观(你说用宏?…… 我什么都没听到)。那么我们为什么不把这种判定也提炼成模板呢?有请 is_detected 出场——

template <typename T>using has_type_t = typename T::type;template <typename T>using has_type = is_detected<has_type_t, T>;

看起来使用 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<T...>>, Op, T...> : std::true_type {};
template <template <typename...> class Op, typename... T>using is_detected = is_detected_impl<void, Op, T...>;

如果仔细看的话,你就能够发现这就是给之前的方法加上了模板模板参数,使得它更容易使用。下面是用 is_detected 判断成员函数是否存在:

template <typename T>using has_get_t = decltype(std::declval<T&>().get());template <typename T>using has_get = is_detected<has_get_t, T>;

当然,is_detected 还可以做到更多,只要你能够写出 Op 的话就有很多可以做的事情,比如说做各种 concept 的检查。除了 is_detected 之外,Detection Idiom 还有 detected_t 和 detected_or 等工具,可以用于在 trait 中实现默认类型,这里就不再展开介绍,感兴趣的话可以到上面的链接里看一下。


- EOF -


推荐阅读  点击标题可跳转

1、C++17之std::optional

2、C++模板进阶指南:SFINAE

3、为什么有序数组比无序数组快?


关于如何优雅地检测类型/表达式有效性,欢迎在评论中和我探讨。觉得文章不错,请点赞和在看支持我继续分享好文。谢谢!


关注『CPP开发者』

看精选C++技术文章 . 加C++开发者专属圈子

↓↓↓


点赞和在看就是最大的支持❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存