前言
在阅读 github 上一些库的源代码时,看到一个很有趣的实现,如何检测一个类型是否是容器,源代码是通过检测类型是否支持 begin
、end
、size
操作来判断。
由于标准库并没有 is_detected
这一实现,所以库作者基本是把 boost 库中的源代码搬了过来,第一次看到时还花了好久时间才弄懂,各种百度最后发现是 boost 库里的东西,于是学习一番并记录下来。
众所周知,模板元编程 AKA 黑魔法,即使短短二十多行代码,都能花掉一下午的时间阅读和各种实验来搞懂,所以在读这篇文章之前,需要你有一定的模板元编程的基础。
源码分析
is_detected
来自 boost 库,功能是检测类型 T
是否含有某种操作,首先需要定义该操作是什么,其次使用 is_detected<Op, T, Args...>
来判断是否支持该操作
一个例子如下:
template<class T>
using clear_t = decltype(std::declval<T>().clear());
template<typename T>
void test(T){
constexpr bool has_clear = is_detected<clear_t, T>::value;
if(has_clear) {
std::cout << "Type support operate clear()" << std::endl;
} else {
std::cout << "Type doesn't support operate clear()" << std::endl;
}
}
int main() {
test(std::string());
test(1);
// output:
// Type support operate clear()
// Type doesn't support operate clear()
return 0;
}
源码分析:boost 库源代码简化之后
// 这里其实不太明白为什么要用可变模板参数,不用其实也能正常运作
template<class...>
struct make_void {
typedef void type;
};
// 对于任何类型 T,detector_t 永远是 void
template<class T>
using detector_t = typename make_void<T>::type;
// 默认版本,Default 实际上是 nonesuch,没有什么实际意义的类型
// 接受四个模板参数,实际上第二个模板参数永远是 void,后续会解释他的作用
template<class Default, class, template<class...> class, class...>
struct detector {
using value_t = false_type;
using type = Default;
};
// 偏特化的版本
template<class Default, template<class...> class Op, class... Args>
struct detector<Default, detector_t<Op<Args...>>, Op, Args...> {
using value_t = true_type;
using type = Op<Args...>;
};
// 在 detected_t 中有用,如果检测有该操作,返回该类型,否则返回 nonesuch 类型
// 该类型显然不支持任何操作,所以对 nonesuch 类型的任何操作都会报错
struct nonesuch {
nonesuch() = delete;
~nonesuch() = delete;
nonesuch(const nonesuch&) = delete;
void operator=(const nonesuch&) = delete;
};
template<template<class...> class Op, class... Args>
using is_detected = typename detector<nonesuch, void, Op, Args...>::value_t;
虽然代码只有短短几行,但是却不容易读懂,接下来我们一步一步进行拆解
template<template<class...> class Op, class... Args>
using is_detected = typename detector<nonesuch, void, Op, Args...>::value_t;
从最上层开始,is_detected
接受两个模板参数,第一个是操作,第二个是参数(包括需要检测的类型和操作的参数),所以数量不固定,是可变模板参数。
接着 is_detected
委托给 detector
template<class Default, class, template<class...> class, class...>
struct detector {
using value_t = false_type;
using type = Default;
};
template<class Default, template<class...> class Op, class... Args>
struct detector<Default, detector_t<Op<Args...>>, Op, Args...> {
using value_t = true_type;
using type = Op<Args...>;
};
默认版本中的 value_t
和 type
都告诉调用者,该类型不支持操作 Op
。因为在特化版本中,如果能够完成 detector_t<Op<Args...>>
的匹配,则使用特化版本,如果不匹配,则使用默认版本。换句话说,这里的语义就是,如果类型支持操作,则匹配特化版本,不支持则匹配默认版本。
默认版本的
type
是nonesuch
,该类型不支持任何操作,所以任何基于该类型的操作都会报错。特化版本的type
是操作的返回类型。
那么这个匹配是如何完成的呢,那就要回到使用方式上了
template<class T>
using clear_t = decltype(std::declval<T>().clear());
//...
constexpr bool has_clear = is_detected<clear_t, T>::value;
//...
可以看到,我们定义的操作 clear_t
是一个模板别名,但是在使用的时候却没有给他定义参数( is_detected<clear_t, T>
)
这里用到了一个模板的知识点,模板参数中的模板 template<template<class> class T>
,说明了类型 T
是一个模板,可以以 func<std::vector>
的形式调用,不用指定 vector
中的模板参数。
随后在 detector<Default, detector_t<Op<Args...>>, Op, Args...>
这里进行了拼接(detector_t<Op<Args...>>
),所以如果一步步展开,结果如下:
template<class T>
using clear_t = decltype(std::declval<T>().clear());
//...
constexpr bool has_clear = is_detected<clear_t, T>::value;
//...
// --> 注意到第二个参数是 void,因为他只能是 void
using is_detected = typename detector<nonesuch, void, clear_t, T>::value_t;
// --> 注意到 detector_t<clear_t<T>> 的推导结果没有意义,但是在这里的作用是进行检测
// clear_t<T> 是否成立,也就是 decltype(std::declval<T>().clear()) 能否执行,如果能
// 说明类型 T 含有该操作
template<class Default, template<class...> class Op, class... Args>
struct detector<Default, detector_t<clear_t<T>>, clear_t, T> {
using value_t = true_type;
using type = clear_t<T>;
};
虽然这个 detector 只有短短二十多行,但用到的知识和技巧却令人叹为观止,第一次遇到读懂都要花一些时间。
最后留个练习,检验对上述代码的理解
练习:检测一个类是否含有操作 func(int, double)
例如:类 A
支持该操作,int
不支持
class A{
public:
int func(int i, double d){
return i + d;
}
};
答案如下:
template<class T, class Arg1, class Arg2>
using func_t = decltype(std::declval<T>().func(
std::declval<Arg1>(),
std::declval<Arg2>()));
template<typename T, class Arg1, class Arg2>
void test(T, Arg1, Arg2){
constexpr bool has_func = is_detected<func_t, T, Arg1, Arg2>::value;
if(has_func) {
std::cout << "Type support operate func()" << std::endl;
} else {
std::cout << "Type doesn't support operate func()" << std::endl;
}
}
int main(){
test(A(), 1, 1.2);
test2(std::string(), 1, 1.2);
// output:
// Type support operate func()
// Type doesn't support operate func()
return 0;
}