C++ 黑魔法初探:boost 库 is_detected

前言

在阅读 github 上一些库的源代码时,看到一个很有趣的实现,如何检测一个类型是否是容器,源代码是通过检测类型是否支持 beginendsize 操作来判断。

由于标准库并没有 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_ttype 都告诉调用者,该类型不支持操作 Op。因为在特化版本中,如果能够完成 detector_t<Op<Args...>> 的匹配,则使用特化版本,如果不匹配,则使用默认版本。换句话说,这里的语义就是,如果类型支持操作,则匹配特化版本,不支持则匹配默认版本。

默认版本的 typenonesuch,该类型不支持任何操作,所以任何基于该类型的操作都会报错。特化版本的 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;
}
上一篇 下一篇

评论 | 0条评论