一、std::void_t
std::void_t是从C++17提供的一个元函数,主要用来在SFINAE应用上更简单方便一些。老规矩先看一下其定义形式:
template< class... >
using void_t = void;
这段代码单纯从代码意义上理解有两个情况:一是它使用了变参模板;二是使用了别名应用。它是一种很简单的应用形式,如果知道变参模板就非常容易理解,其实就是把其它N个数据类型定义为void类型。这块知识对于模板老鸟儿来说是不值一哂的,即使一些新手也感觉不到它的复杂和难度,但随之而来的就是,这玩意儿有啥用。太简单的东西,一般来说会让新人无所适从。拿到了所谓的斧头,却不知道该如何用。
二、模板中的应用
进行模板编程特别是元编程的开发者来说,有一个问题。那就是如何在编译期进行条件逻辑的转换和数据类型的处理。还是那句话,对于普通的编程(或者说非编译期展开的编程),条件判断非常简单if等语句可以轻松拿捏。而数据类型的处理对C++这种强类型语言来说更是方便处理。但麻烦就在于编译期很多东西都是不确定的,它未到达运行期,所以根本没办法使用运行时的那一套手段。这也是初学模板和元编程时,很多程序员感到头疼的地方。
逻辑处理仍然是开发者认知的那个逻辑处理,是与非,但形成是与非的这种手段则变了。如果单纯使用SFINAE技术,复杂性和难于理解性和不方便性,几乎同时存在。前面提到过,无论哪种语言整体的方向是朝着简单化的方向进化,C++也是如此。
std::void_t就是为了在SFINAE编程中提供方便,或者说在C++20概念Concepts出现前对SFINAE技术的一种优化手段。有人说可不可以不使用void,使用int或其它的类型来这样使用呢?当然可以。但又引入了其它的限制条件和更多的判断,因为int或其它类型都不如void在C++语言中应用更广泛。
回到如何应用的话题,既然知道它是在SFINAE中应用,那么一个最简单的应用就是判断表达式的合法性,从而进行逻辑选择,特别是在一些不求值的语句中,其更具有优势,如decltype和declval。看一个简单的应用场景:
template< class, class = void >
struct has_type_member : std::false_type { };
template< class T >
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { };
是不是非常熟悉,在前面的不少的文章中都有类似的代码,用来判断和处理类中是否存在某个成员(可参看前面的“一步一步写线程之八线程池的完善之二数据结构封装”)。同样的手段,稍微变换一下就可以做数据类型的检测是否为希望的类型,如果不是,则编译失效,抛出一个错误。同样不同的逻辑选择用在数据类型的返回上,可以得到不同的数据类型。
这也是前面提到的数据类型的检测处理和逻辑选择。
这里有一个小细节,为什么使用变参模板呢?试想一下,有没有这种开发场景,一个类中,同时存在N个成员,有时候儿需要判断它们同时存在呢?如果明白了这种情况就明白了为什么是变参模板了。否则的话,就得一个个的写,一个一个的判断。亦或者,开发者自己另外实现类似的差别方法。
大家是不是突然觉得,这货是不是和std::enable_if的用法有点类似,只不过一个是元函数一个是元类型。
这里再顺带说明一下,上面的代码中用到了std::false_type,它是由下面的定义而来 :
template< bool B >
using bool_constant = integral_constant<bool, B>;
using true_type = std::integral_constant<bool, true>
using false_type = std::integral_constant<bool, false>
注意,它也是在C++17支持的。所以说很多的技术都是互相关联产生的,小小的组合可能产生大大的威力。
三、例程
来看一下cppreference上的例程:
#include <iomanip>
#include <iostream>
#include <map>
#include <type_traits>
#include <vector>
// 检查类型是否有 begin() 和 end() 成员函数的变量模板
template<typename, typename = void>
constexpr bool is_iterable = false;
template<typename T>
constexpr bool is_iterable<
T,
std::void_t<decltype(std::declval<T>().begin()),
decltype(std::declval<T>().end())
>
> = true;
// 迭代器特征,其 value_type 是被迭代容器的 value_type,
// 也支持 value_type 为 void 的 back_insert_iterator
template<typename T, typename = void>
struct iterator_trait : std::iterator_traits<T> {};
template<typename T>
struct iterator_trait<T, std::void_t<typename T::container_type>>
: std::iterator_traits<typename T::container_type::iterator> {};
class A {};
#define SHOW(...) std::cout << std::setw(34) << #__VA_ARGS__ \
<< " == " << __VA_ARGS__ << '\n'
int main()
{
std::cout << std::boolalpha << std::left;
SHOW(is_iterable<std::vector<double>>);
SHOW(is_iterable<std::map<int, double>>);
SHOW(is_iterable<double>);
SHOW(is_iterable<A>);
using container_t = std::vector<int>;
container_t v;
static_assert(std::is_same_v<
container_t::value_type,
iterator_trait<decltype(std::begin(v))>::value_type
>);
static_assert(std::is_same_v<
container_t::value_type,
iterator_trait<decltype(std::back_inserter(v))>::value_type
>);
}
通过std::void_t检测类型是否有迭代器的支持。其实它可以拓展到很多,包括智能指针、运算符(++等)甚至是否包含类型嵌套等等。开发者可以在实际的开发过程中活学活用,大胆尝试,不行再写别的方法。
再看一个类型推导的例程:
template<typename T, typename = void>
struct resultOf {
using type = void;
};
template<typename T>
struct resultOf<T, std::void_t<decltype(std::declval<T>().resultT())>> {
using type = decltype(std::declval<T>().resultT());
};
template<typename T>
using resultOf_t = typename resultOf<T>::type;
template<typename T>
resultOf_t<T> CheckType(const T& t) {
if constexpr (!std::is_same_v<resultOf_t<T>, void>) {
return t.resultT(); //通过T中的resultT来获取类型
}
}
多看多用慢慢就会掌握门道,初窥门径后就可以自由的进行组合应用了。
四、总结
std::void_t更好的是在模板和元编程中应用,这也是C++的一个发展的重点。毕竟泛型编程是每个语言都不能舍弃的情怀。所以对大多数程序员来说,std::void_t这个元函数属于一种应用非常少的情况。可以先了解一下,然后再真正遇到后再认真学习。
在这篇普及的文章基础上,后面将对enable_if与std::void_t以及C++20中的std::integral进行一次系统的分析说明。这样,会让大家能从整体到细节上对模板和元编程中的相关用法有一个清晰的认识。