探测成员
基于SFINAE,判断一个给定类型T,是否含有名为x的成员。
探测类型成员
判断一个给定类型T,是否含有类型成员size_type,源码如下:
#include <type_traits>
#include <iostream>
#include <vector>
template <typename ...>
using void_t = void;
template <typename , typename = void_t<>>
struct has_sizetype_t : std::false_type {};
template <typename T>
struct has_sizetype_t<T, void_t<typename T::size_type>> : std::true_type {};
int main(int argc, char **argv) {
std::cout << (has_sizetype_t<int>::value ? "true" : "false") << std::endl;
std::cout << (has_sizetype_t<std::vector<int>>::value ? "true" : "false") << std::endl;
return 0;
}
注意:如果类型成员为private成员,萃取模板没有访问该类型的特殊权限,has_sizetype_t会返回false。
对于普通类型,上述模板能够准确做出判断,但如果模板参数为引用类型,如std::vector<int>&,上述模板处理结果确是false。为了能兼容这种情况,可以在偏特化使用std::removereference,如下:
template <typename T>
struct has_sizetype_t<T, void_t<typename std::remove_reference<T>::type::size_type>> : std::true_type {};
对于注入类的名字,上述检测模板也会返回true,如下:
struct size_type {};
struct size_able : size_type {};
//...
std::cout << (has_sizetype_t<size_able>::value ? "true" : "false") << std::endl;
探测任意类型成员
has_sizetype_t能够有效的判断一个类型T是否包含类型成员size_type,如果把size_type换成其他类型呢?最简单也是最暴力的做法,就是仿照has_sizetype_t再实现一个萃取模板。但为每个类型成员都实现一个萃取模板,不太合理,把类型成员参数化,才是最优的选择。然而不幸的是,目前还没有语言机制可以被用来描述“潜在” 的名字。但是可以通过宏来实现这一功能,源码如下:
#include <type_traits>
#include <iostream>
#include <string>
#include <vector>
template <typename ...>
using void_t = void;
#define define_has_member_type(member_type) \
template <typename , typename = void_t<>> \
struct has_##member_type##_t : std::false_type {}; \
\
template <typename T> \
struct has_##member_type##_t<T, void_t<typename std::remove_reference<T>::type::member_type>> : std::true_type {};
//struct has_##member_type##_t<T, void_t<typename T::member_type>> : std::true_type {};
define_has_member_type(size_type)
define_has_member_type(str_type)
class mystring {
public:
using str_type = std::string;
};
class myint {
public:
using int_type = int;
};
int main(int argc, char **argv) {
std::cout << (has_str_type_t<mystring&&>::value ? "true" : "false") << std::endl;
std::cout << (has_str_type_t<myint>::value ? "true" : "false") << std::endl;
std::cout << (has_size_type_t<int>::value ? "true" : "false") << std::endl;
std::cout << (has_size_type_t<std::vector<int>>::value ? "true" : "false") << std::endl;
std::cout << (has_size_type_t<std::vector<int>&&>::value ? "true" : "false") << std::endl;
return 0;
}
探测非类型成员
对上述萃取模板做简单的修改,便可以得到探测非类型成员的萃取模板,如下:
#include <type_traits>
#include <iostream>
template <typename ...>
using void_t = void;
#define define_has_member(member) \
template <typename, typename = void_t<>> \
struct has_##member##_t : std::false_type {}; \
\
template <typename T> \
struct has_##member##_t<T, void_t<decltype(&T::member)>> : std::true_type {};
enum myenum {
failed = 0,
succeeded = 1
};
class myclass {
public:
int value;
myenum status;
static int instance_number;
};
class myclass2 {
public:
using status = myenum;
};
define_has_member(value)
define_has_member(status)
define_has_member(instance_number)
int main(int argc, char **argv) {
std::cout << (has_status_t<myclass2>::value ? "true" : "false") << std::endl;
std::cout << (has_status_t<std::true_type>::value ? "true" : "false") << std::endl;
std::cout << (has_value_t<myclass>::value ? "true" : "false") << std::endl;
std::cout << (has_status_t<myclass>::value ? "true" : "false") << std::endl;
std::cout << (has_instance_number_t<myclass>::value ? "true" : "false") << std::endl;
return 0;
}
偏特化模板参数中为什么使用void_t<decltype(&T::member)>,而非void_t<decltype(T::member)>?因为使用后者探测数据成员没有问题,但探测成员函数就无能为力了。使用该萃取模板,还需要注意以下几点:
成员必须是非类型成员以及非枚举成员,否则&T::member失效,所以has_kind_type_t,has_ultrasonic_t均检测失败
成员必须是public成员
member必须没有歧义,对于has_kind_t<bat>::value,has_name_t<bat>::value,has_move_t<bat>::value,编译其无法确认kind为mammals::kind,还是bird::kind,name为mammals::name,还是bird::name,move为mammals::move,还是bird::move,所以均检测失败
如果存在重载函数,检测失败,如bat拥有两个search版本,所以has_search_t检测失败
针对重载函数探测失效的问题,目前的方案只能针对不同的函数实现不同的的萃取模板。如下:
template <typename, typename = void_t<>>
struct has_search_void_t : std::false_type {};
template <typename T>
struct has_search_void_t<T, void_t<decltype(std::declval<T>().search())>> : std::true_type {};
template <typename, typename = void_t<>>
struct has_search_int_t : std::false_type {};
template <typename T>
struct has_search_int_t<T, void_t<decltype(std::declval<T>().search(0))>> : std::true_type {};
下面是两种失败的尝试方案:
//方案1
//ERROR:
//: error: expected ')'
// std::cout << (has_search_t<bat, int>(nullptr)::value ? "true" : "false") << std::endl;
//to match this '('
// std::cout << (has_search_t<bat, int>(nullptr)::value ? "true" : "false") << std::endl;
template <typename T, typename ...Args, typename = void_t<decltype(std::declval<T>().search(std::declval<Args>()...))>>
constexpr std::true_type has_search_t(void *);
template <typename, typename ...Args>
constexpr std::false_type has_search_t(...);
//...
std::cout << (has_search_t<bat, int>(nullptr)::value ? "true" : "false") << std::endl;
//方案2
//ERROR:
//template parameter pack must be the last template parameter
// template <typename, typename ...Args, typename = void_t<>>
//error: expected expression
// struct has_search_t <T, ...Args, void_t<decltype(std::declval<T>().search(std::declval<Args>()...))>>: std::true_type {};
//expected unqualified-id
// struct has_search_t <T, ...Args, void_t<decltype(std::declval<T>().search(std::declval<Args>()...))>>: std::true_type {};
template <typename, typename = void_t<>>
struct has_search_void_t : std::false_type {};
template <typename T>
struct has_search_void_t<T, void_t<decltype(std::declval<T>().search())>> : std::true_type {};
除了类的类型成员,变量成员,函数成员外,还可以用于探测表达式,甚至是多个表达式的组合,下面是两个例子:
template <typename ...>
using void_t = void;
//表达式
template <typename, typename, typename = void_t<>>
struct has_less_t : std::false_type {};
template <typename T1, typename T2>
struct has_less_t<T1, T2, void_t<decltype(std::declval<T1>() < std::declval<T2>())>> : std::true_type {};
//表达式组合
template <typename, typename = void_t<>>
struct has_calc_t : std::false_type {};
template <typename T>
struct has_calc_t<T, void_t<decltype(std::declval<T>() + std::declval<T>(), std::declval<T>() - std::declval<T>(), std::declval<T>() * std::declval<T>(), std::declval<T>() / std::declval<T>())>> : std::true_type {};
用泛型 Lambda 探测成员
使用萃取的实现(二)【将泛型 Lambdas 用于 SFINAE】中的lambda表达式,也可以实现成员探测,源码如下:
constexpr auto has_size_type = is_valid([](auto &&x)->typename std::decay_t<decltype(x)>::size_type {});
template <typename T>
using has_size_type_t = decltype(has_size_type(std::declval<T>()));
constexpr auto has_begin = is_valid([](auto &&x)->decltype(x.begin()){});
//constexpr auto has_begin = is_valid([](auto &&x)->decltype(std::declval<decltype(x)>().begin()){});
template <typename T>
using has_begin_t = decltype(has_begin(std::declval<T>()));
int main(int argc, char **argv) {
std::cout << (has_size_type_t<std::string>::value ? "true" : "false") << std::endl;
std::cout << (has_size_type_t<int>::value ? "true" : "false") << std::endl;
std::cout << (has_begin_t<std::string>::value ? "true" : "false") << std::endl;
std::cout << (has_begin_t<int>::value ? "true" : "false") << std::endl;
return 0;
}
其它的萃取技术
If-Then-Else
如何根据数值的大小,自动选择合适的整型类型?首先定义一个模板if_then_else模拟选择行为,该模板接收三个模板参数,并且第一个参数为bool常量表达式,剩余的两个参数为类型参数,由第一个参数的值决定哪个类型参数生效;然后,定义一个参数为整型数值的模板,调用if_then_else,推断出合适的类型(由于实现不知道数值的合适类型,因此模板参数的类型使用了auto,需要c++17才能支持)。代码如下:
template <bool COND, typename TrueType, typename FalseType>
struct if_then_else { using type = TrueType; };
template <typename TrueType, typename FalseType>
struct if_then_else<false, TrueType, FalseType> { using type = FalseType; };
template <auto N>
struct small_int {
using type = typename if_then_else<N <= std::numeric_limits<char> ::max(), char,
typename if_then_else<N <= std::numeric_limits<short> ::max(), short,
typename if_then_else<N <= std::numeric_limits<int> ::max(), int,
typename if_then_else<N <= std::numeric_limits<long>::max(), long,
typename if_then_else<N <= std::numeric_limits<long long>::max(), long long,
void>::type>::type>::type>::type>::type;
};
需要注意的是,和常规的 C++ if-then-else 语句不同,在最终做选择之前,then 和 else 分支 中的模板参数都会被计算,因此两个分支中的代码都不能有问题,否则整个程序就会有问题。
探测不抛出异常的操作
有时候我们需要判断一些操作是否会抛出异常,这时我们需要用到noexcept操作符,该操作符对表达式进行编译时检查,如果表达式不抛出任何异常返回true,否则返回false。主要应用场景如下:
- 显示指明函数是否会抛出异常:
noexcept和noexcept(true)等效,表示该函数不抛出任何异常;noexcept(false)表示有可能抛出异常
- 阻止异常传播和扩散,出现异常直接调用std::terminate终止进程
#include <iostream>
void func1() {
throw 1;
}
void func2() {
func1();
}
void func3() noexcept {
func1();
}
int main(int argc, char **argv) {
try {
func1();
} catch(...) {
std::cout << "func1" << std::endl;
}
try {
func2();
} catch(...) {
std::cout << "func2" << std::endl;
}
try {
func3(); //阻止异常传播,程序crash
} catch(...) {
std::cout << "func3" << std::endl;
}
return 0;
}
- 用于模板,增强c++泛型能力
#include <type_traits>
#include <iostream>
template <typename T, typename = std::void_t<>>
struct has_noexcept_move_constructor : std::false_type {};
template <typename T>
struct has_noexcept_move_constructor<T, std::void_t<decltype(T(std::declval<T>()))>> : std::bool_constant<noexcept(T(std::declval<T>()))> {};
class A {
public:
A(A &&) {
throw 1;
}
};
class B {
public:
B(B &&) = default;
};
int main(int argc, char **argv) {
std::cout << (has_noexcept_move_constructor<A>::value ? "true" : "false") << std::endl;
std::cout << (has_noexcept_move_constructor<B>::value ? "true" : "false") << std::endl;
return 0;
}
萃取的便捷性
关于萃取最大的问题便是繁琐,对于萃取类型的使用需要添加::type后缀,而且在依赖上下文中,还需要一个typename前缀,例如:
template <typename T>
void print_type_name() {
std::cout << typeid(typename std::remove_reference<T>::type).name() << std::endl;
}
别名模板和萃取
使用using创建别名,可以简化类型名称,如下:
template <typename T>
using rmref = typename std::remove_reference<T>::type;
template <typename T>
void print_type_name() {
std::cout << typeid(rmref<T>).name() << std::endl;
}
从c++14开始,直接为之引入了相应的别名模板,以_t结尾,对于“typename std::remove_reference<T>::type”,其别名为“std::remove_reference_t<T>”。
变量模板和萃取
对于返回数值的萃取需要使用一个::value来生成萃取的结果,如下:
template <typename T1, typename T2>
void print_diff() {
std::cout << (std::is_same<T1, T2>::value ? "true" : "false") << std::endl;
}
针对上面这种情况,可以使用constexpr修饰的变量模板进行简化(需要在C++14之后),如下:
template <typename T1, typename T2>
constexpr bool issame = std::is_same<T1, T2>::value;
template <typename T1, typename T2>
void print_diff() {
std::cout << (issame<T1, T2> ? "true" : "false") << std::endl;
}
从c++17开始,直接引入了与之对应的变量模板,以_v结尾,对于“std::is_same<T1, T2>::value”,对应的变量模板为“std::is_same_v<T1, T2>”。
类型分析
判断基础类型
判断一个类型是否是基础类型,如int是否是基础类型。代码如下:
template <typename T>
struct is_fundamental : std::false_type {};
template <>
struct is_fundamental<int> : std::true_type {};
template <>
struct is_fundamental<double> : std::true_type {};
int main(int argc, char **argv) {
std::cout << (is_fundamental<double>::value ? "true" : "false") << std::endl;
std::cout << (is_fundamental<int>::value ? "true" : "false") << std::endl;
return 0;
}
为了能够正确的识别出每一个基础类型,只能针对每个基础类型实现一遍is_fundamental。通过宏尽管能少些一些代码,但本质上依旧是对基础类型的穷举。
#define IS_FUNDAMENTAL(T) template <> struct is_fundamental<T> : std::true_type {};
IS_FUNDAMENTAL(float)
IS_FUNDAMENTAL(unsigned long)
判断复合类型
复合类型是由其它类型构建出来的类型。简单的复合类型包含指针类型,左值以及右值引用 类型,指向成员的指针类型(pointer-to-member types),和数组类型。
//指针
template <typename T>
struct is_pointer : std::false_type {};
template <typename T>
struct is_pointer<T *> : std::true_type {};
//左值引用
template <typename T>
struct is_lvalue_reference : std::false_type {};
template <typename T>
struct is_lvalue_reference<T &> : std::true_type {};
//右值引用
template <typename T>
struct is_rvalue_reference : std::false_type {};
template <typename T>
struct is_rvalue_reference<T &&> : std::true_type {};
//数组
template <typename T>
struct is_array : std::false_type {};
template <typename T>
struct is_array<T[]> : std::true_type {};
template <typename T, std::size_t N>
struct is_array<T[N]> : std::true_type {};
//指向成员的指针
template <typename T>
struct is_member_pointer : std::false_type {};
template <typename T, typename C>
struct is_member_pointer<T C::*> : std::true_type {};
struct S {
int a = 10;
int b = 20;
float func1(void) { return 0; }
float func2(void) { return 1; }
};
using mem_data_ptr_t = int S::*;
using mem_func_ptr_t = float S::*;
int main(int argc, char **argv) {
std::cout << (is_pointer<int>::value ? "true" : "false") << std::endl;
std::cout << (is_pointer<int *>::value ? "true" : "false") << std::endl;
int *pi = &argc;
std::cout << (is_pointer<decltype(pi)>::value ? "true" : "false") << std::endl;
std::cout << std::endl;
std::cout << (is_lvalue_reference<int>::value ? "true" : "false") << std::endl;
std::cout << (is_lvalue_reference<int &>::value ? "true" : "false") << std::endl;
int &lri = argc;
std::cout << (is_lvalue_reference<decltype(lri)>::value ? "true" : "false") << std::endl;
std::cout << std::endl;
std::cout << (is_rvalue_reference<int>::value ? "true" : "false") << std::endl;
std::cout << (is_rvalue_reference<int &&>::value ? "true" : "false") << std::endl;
std::cout << (is_rvalue_reference<decltype(std::list<int>())>::value ? "true" : "false") << std::endl;
//不太确定为什么decltype(std::list<int>())得到的就不是右值引用类型
std::cout << typeid(decltype(std::list<int>())).name() << std::endl;
std::list<int> li;
std::cout << (is_rvalue_reference<decltype(std::move(li))>::value ? "true" : "false") << std::endl;
std::cout << typeid(decltype(std::move(li))).name() << std::endl;
std::cout << std::endl;
std::cout << (is_array<int>::value ? "true" : "false") << std::endl;
std::cout << (is_array<int[]>::value ? "true" : "false") << std::endl;
std::cout << (is_array<int [3]>::value ? "true" : "false") << std::endl;
int arr[10]; //int arr[0]; decltype(arr)得到的类型时A0_i,不是数组
std::cout << (is_array<decltype(arr)>::value ? "true" : "false") << std::endl;
std::cout << typeid(int[3]).name() << std::endl;
std::cout << typeid(decltype(arr)).name() << std::endl;
std::cout << std::endl;
using mem_string_ptr_t = std::string S::*;
//虽然这个类型不存在,但结果依然返回了true,标注库意识如此
std::cout << (is_member_pointer<mem_string_ptr_t>::value ? "true" : "false") << std::endl;
std::cout << (is_member_pointer<mem_data_ptr_t>::value ? "true" : "false") << std::endl;
std::cout << (is_member_pointer<mem_func_ptr_t>::value ? "true" : "false") << std::endl;
}
识别函数类型
在讨论如何识别函数类型时,先考虑如何定义一个函数类型。下面是函数类型定义的相关代码:
#include <iostream>
int func_instance(int arg) {
std::cout << arg << std::endl;
return 0;
}
int main(int argc, char **argv) {
typedef int (func_t)(int);
func_t *func1 = func_instance; ✅
func1(12);
typedef int (*func_pt)(int);
func_pt func2 = func_instance; ✅
func2(14);
return 0;
}
通过上面的例子,可以看出:只要不同函数类型的返回值和入参一致,既可以表示同一个类型,名称不是特别重要。因此,识别函数类型时,只用到了返回值和入参,只要类型符合Ret(Args...)格式,该类型就是函数类型。这个格式很像去掉了名称的函数类型定义。下面是函数类型识别源码:
#include <type_traits>
#include <iostream>
#include <cstdio>
template <typename T>
struct is_function : std::false_type { constexpr static int flag = 0; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args...)> : std::true_type { constexpr static int flag = 1; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args..., ...)> : std::true_type { constexpr static int flag = 2; };
class A {
public:
A(){}
static A &GetInstance() { static A _inst; return _inst; }
void Set(int) {}
int Get(void) const { return 0; }
int Func(int a) const & { return a; }
};
struct B
{
static int foo();
int fun() const&;
};
int main(int argc, char **arg) {
std::cout << (is_function<int(int)>::value ? "true" : "false") << " " << is_function<int(int)>::flag << std::endl;
std::cout << (is_function<decltype(printf)>::value ? "true" : "false") << " " << is_function<decltype(printf)>::flag << std::endl;
std::cout << (is_function<decltype(&A::Set)>::value ? "true" : "false") << " " << is_function<decltype(&A::Set)>::flag << std::endl;
std::cout << (is_function<const int(void)>::value ? "true" : "false") << " " << is_function<const int(void)>::flag << std::endl;
std::cout << (is_function<decltype(&A::Get)>::value ? "true" : "false") << " " << is_function<decltype(&A::Get)>::flag << std::endl;
std::cout << (is_function<decltype(A::GetInstance)>::value ? "true" : "false") << " " << is_function<decltype(A::GetInstance)>::flag << std::endl;
std::cout << (is_function<decltype(&B::fun)>::value ? "true" : "false") << " " << is_function<decltype(&B::fun)>::flag << std::endl;
std::cout << (is_function<int(int)const>::value ? "true" : "false") << " " << is_function<int(int)const>::flag << std::endl;
std::cout << (is_function<int(int) &>::value ? "true" : "false") << " " << is_function<int(int) &>::flag << std::endl;
std::cout << (std::is_function<decltype(&A::Get)>::value ? "true" : "false") << std::endl;
return 0;
}
需要注意几个问题:
- is_function识别的是函数类型,对于函数需要通过decltype推断出其函数类型,如decltype(printf)
- is_function可以识别类的静态成员函数,无法识别非静态成员函数
- is_function不能处理所有函数类型,因为有些函数还包含const和volatile,到c++17之后,还包含noexcept操作符,如int(int) const
- 此外还应该处理函数的引用情况,关于函数的引用,解释如下(摘自c 成员函数声明()后加&或&&表示什么):
C++中,成员函数声明后添加
&
或&&
表明这个成员函数是针对左值对象还是右值对象进行操作的。具体来说,在成员函数声明后加上&
表示该成员函数只能被左值对象调用、而加上&&
表示该成员函数只能被右值对象调用。这种技术是C++11引入的,用于支持移动语义和更精细地控制对象的行为。
更多is_function的重载,如下(非全部):
template <typename Ret, typename ...Args>
struct is_function<Ret(Args...) const> : std::true_type { constexpr static int flag = 3; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args..., ...) const> : std::true_type { constexpr static int flag = 4; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args...) volatile> : std::true_type { constexpr static int flag = 5; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args..., ...) volatile> : std::true_type { constexpr static int flag = 6; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args...) const volatile> : std::true_type { constexpr static int flag = 7; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args..., ...) const volatile> : std::true_type { constexpr static int flag = 8; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args...) &> : std::true_type { constexpr static int flag = 9; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args..., ...) &> : std::true_type { constexpr static int flag = 10; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args...) const &> : std::true_type { constexpr static int flag = 11; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args..., ...) const &> : std::true_type { constexpr static int flag = 12; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args...) volatile &> : std::true_type { constexpr static int flag = 13; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args..., ...) volatile &> : std::true_type { constexpr static int flag = 14; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args...) const volatile &> : std::true_type { constexpr static int flag = 15; };
template <typename Ret, typename ...Args>
struct is_function<Ret(Args..., ...) const volatile &> : std::true_type { constexpr static int flag = 16; };
以上仅是基础实现,libc++,libstdc++,MS stl还有更为简单的实现,如下:
template<class T>
struct is_function : std::integral_constant<
bool,
!std::is_const<const T>::value && !std::is_reference<T>::value
> {};
判断class类型
原理:只有class类型才可以被用于指向成员的指针类型,即对于T Y::*一类的类型结构,Y只能是class类型,T可以选择任何类型。
#include <type_traits>
#include <iostream>
template <typename T, typename = std::void_t<>>
struct is_class : std::false_type {};
template <typename T>
struct is_class<T, std::void_t<int T::*>> : std::true_type {};
struct s {};
class c {};
union u {};
enum e {};
int main(int argc, char **argv) {
std::cout << (is_class<s>::value ? "true" : "false") << std::endl;
std::cout << (is_class<c>::value ? "true" : "false") << std::endl;
std::cout << (is_class<u>::value ? "true" : "false") << std::endl;
std::cout << (is_class<e>::value ? "true" : "false") << std::endl;
auto f = []{};
std::cout << (is_class<decltype(f)>::value ? "true" : "false") << std::endl;
std::cout << (is_class<uint64_t>::value ? "true" : "false") << std::endl;
std::cout << (is_class<int>::value ? "true" : "false") << std::endl;
return 0;
}
需要注意两点以下两点:
- c++语言指出,lambda 表达式的类型是“唯一的,未命名的,非枚举 class 类型”,使用is_class萃取lambda表达式,得到的结果也是true
- int T::*表达式同样适用于 union类型