Bootstrap

一文速通C++类型萃取type traits

类型萃取(编译期)

编译期获知变量的类型:

有时,对于同一个函数(或者算法),他们的操作是类似的,但是对于不同的类型,会有一些细微的改变,这个时候类型萃取就能比较简单地实现这个需求。

类型萃取不仅可以检查类型属于哪一种。还可以对类型进行修改,比如添加const,添加引用等

integral_constant

引子: 模板参数

类型 或 常量

在模板编程中,模板参数只能使用 类型 或 字面常量

template<bool bool_val, typename T>
class A ...

A<true, int> aInt;   // 正确 字面常量
A<false, int> bInt;  // 正确 字面常量

bool bool_val_1 = false;
A<bool_val_1, int> aInt; // 编译出错, 需要声明 字面常量表达式 constexpr

constexpr bool bool_val_2 = true;
A<bool_val_2, int> aInt;   // 正确 字面常量表达式
  1. 类型
  • int, double, float
    • 自定义类型class。
  • 以及,后续会介绍的:字面常量的类型std::integral_constant<typename _Tp, _Tp __v>::value_type
  1. 常量:

    <>中除了类型,还可以要添加值(字面常量)。

    • 字面常量表达式 constexpr bool cond = true;

    • 字面常量:"value"1'c'true

不便之处

需求:即想要 根据传入的类型去实例化对象,并且传入该类型的默认值。

解决:两个参数分别指明类型字面常量。显式标注模板参数类型,偏特化。

  • 两个参数,
    • 一个指定类型
    • 一个指定字面常量
  • 例如
    • template<typename T, T X>
    • func01<int, 1>() func01<double, 1.0>()

偏特化(显式固定的值)

  • 实现处:需要提供偏特化——大量的冗余重复。
  • 使用处:方便。
// 要求
//  - 创建 T 类型的变量
//  - 使用 默认值 bool/int X

// 不显示指定默认值类型
template<typename T, T X>
void mFunc(){ }
// 特例:偏特化,显示地指定类型(优先使用)
template<typename T, bool X>
void mFunc(){ }

// 缺点: 虽然使用时无感,但是要支持其他通用的类型的常量,往往需要把同样的代码抄一遍
mFunc<double, 10.0>(); // 使用typename T, T X
mFunc<int, 10>();    // 使用偏特化
mFunc<bool, 10>();   // 不支持
类型字面常量

用一个参数,同时表示 类型字面常量

  1. 兼具两者的integral_constant类型字面常量

更好的办法,利用一个抽象同时把类型字面常量(默认值)表示起来,可以同时允许 创建 T 类型的变量 以及 使用 默认值

使用integral_constant

  • template<typename T>
  • func01<std::integral_constant<bool, false>>();具体见下文

从抽象上来说,代码的实现方直接把类型字面常量抽象成一个模板,并且type/value可以引用到相应的类型和值。实现处少写,使用处多写。

// 答:配合integral_constant使字面量变成类型typename
template<typename constexpr_val>
void mFunc(){ }

// 字面量无法作为类型
mFunc<true>();
mFunc<false>();
// 使用配合integral_constant使字面量变成类型typename
// 详情见下文
typedef std::integral_constant<bool, false> false_type;
mFunc<false_type>();

原理

这个类是type_traits的基石,基本上type_traits都直接或间接继承自integral_constant。提供了

  • 一个变量:静态编译期常量 value

  • 两个 typedef

    • value_type,对T 类型取别名
    • type,对 integral_constant 这个类型常量本身取别名
  • 两个 操作符重载

    • value_type()operator()()
    • 均返回 静态编译期常量 value
/// integral_constant
template<typename _Tp, _Tp __v>
struct integral_constant {
    // 注意static
    static constexpr _Tp                  value = __v;
    typedef _Tp                           value_type;
    typedef integral_constant<_Tp, __v>   type;
    constexpr operator value_type() const { return value; }
    constexpr value_type operator()() const { return value; }
};

// 再次声明: 类外声明 静态成员变量(静态变量 要在类外声明)
// 实际上,在c++11中constexpr应该就已经不需要重新声明定义了
template<typename _Tp, _Tp __v>
constexpr _Tp integral_constant<_Tp, __v>::value;
struct mStruct {
    // 注意static
    typedef int                           value_type;
};

int main() {
    mStruct::value_type mInt = 10;

typedef

使用 std::integral_constant 对 “类型的常量” 进行替换。

// 重命名 std::integral_constant
typedef std::integral_constant<bool, false> false_type;

// SomeCode<int, false> aInst; 被替换为
func02<false_type> aInst;
// 即:
func02<std::integral_constant<bool, false>> aInst;
// 重命名 std::integral_constant
typedef std::integral_constant<int, 10> bint10_type;

// SomeCode<int, 10> bInst; 被替换为
func02<bint10_type> bInst;
// 即:
func02<std::integral_constant<int,10>> bInst;

一些integral_constant示例:

/// The type used as a compile-time boolean with true/false value.
typedef integral_constant<bool, true>     true_type;
typedef integral_constant<bool, false>    false_type;
// __bool_constant这是一个辅助类型重定义,可以用在模板函数重载中
template<bool __v>
using __bool_constant = integral_constant<bool, __v>;

type_traits中,大部分类都直接或间接继承自integral_constant

  • 凡是带前缀is_*的类的偏特化特化基本都是继承自true_type
  • 例如 __is_pointer_helper_Tp*匹配失败就会继承false_type,调用操作符或 ::value得到的自然就是 false,反之,成功匹配就返回 true。
template<typename>
struct __is_pointer_helper : public false_type { };

template<typename _Tp>
struct __is_pointer_helper<_Tp*> : public true_type { };

使用

客户端(使用函数的地方):使用方式上就会有点麻烦了。

  • 获取 类型——变量 ::value_type
  • 获取 静态编译期常量——变量::value,操作符重载value_type() ,操作符重载 operator()()
  • 获取 类型常量 integral_constant 本身—— 变量 ::type
template<typename integral>
void func02(T param) {
    // 使用 类型常量integral_constant
    typedef typename integral::type type;
    // 使用 类型
    typedef typename integral::value_type value_type;
    value_type mVal = 10;
    // 使用 字面常量
    value_type mVal2 = integral::value;
    
};

获取类型

decay<> 还原

去除cv限定符,引用,etc。

用途

std::decay就是对一个类型进行退化处理:把各种引用,cosnt,volitale,去掉。这样就能通过std::is_same正确识别出加了引用的类型了

  • 在模板里可以通过std::is_same判断模板的类型,从而实现对不同类型的区别对待
  • 在对类型要求不是非常严格的情况下,可以使用std::decay把类型退化为基本形态,结合std::is_same用,可以判断出更多的情况
应用

涉及到模板类型时,需要 typename关键字(加了肯定没错,不加可能有错)

  • typename decay<decltype(xx)>::type
  • typename decay<T::value_type>::type

cpp17,支持decay_t。

  • 应用:decay_t<T::value_type> decay_t<decltype(xx)>

  • 这种 _t 基本就是对 integral_constant::type 成员 取别名,

  • 它做的事就是 using decay_t = typename std::decay<decltype(t)>::type

    std::decay_t<decltype(t)> 等价于 typename std::decay<decltype(t)>::type

    template<typename _Tp>
    using decay_t = typename decay<_Tp>::type;
    
实现
template< class T >
struct decay {
private:
    typedef typename std::remove_reference<T>::type U;
public:
    typedef typename std::conditional< 
        std::is_array<U>::value,
        typename std::remove_extent<U>::type*,
        typename std::conditional< 
            std::is_function<U>::value,
            typename std::add_pointer<U>::type,
            typename std::remove_cv<U>::type
        >::type
    >::type type;
};

decltype

用途

decltype用途:编译期获知 变量/表达式 的类型,从而 声明/声明变量 或 进行类型萃取

  • 用于获得与某一 变量
  • 表达式 的类型(甚至捕获栈上变量lambda的类型 )

decltype(变量名)decltype(表达式)

  • 表达式

    • 不执行:仅仅获得表达式的类型

      例如:函数不被调用,仅仅获得返回值类型

  • 变量

    • 若希望得到引用类型 例如int &,同样通过 decltype((变量名))

      强制编译器将变量视为表达式,从而得到引用类型

    • 左值引用,是一定需要初始化的。

类型推导4规则

decltype(expression)

  • 没有带括号的标记符表达式 / 类成员访问表达式

    expression 所命名的实体的类型。

    • 包括const等cv限定符,以及引用&。

decltype((expression))

  • 表示添加引用。注:左值引用类型是一定要初始化的。

decltype(被重载的函数)

  • 则会导致编译错误。

变量:假设变量e的类型是T

变量e的类型表达式类型
将亡值decltype(e)T&&
左值decltype(e)T&
右值decltype(e)T
decltype((e))T&
从表达式推导
decltype(auto) 万能推导

来自变量 / 返回值,decltype(auto)用来声明变量以及指示函数返回类型。

被用于声明变量时,该变量必须立即初始化。

在推导函数返回值类型时,

  • 先用返回值表达式替换decltype(auto)当中的auto,
  • 再根据decltype的语法规则来确定函数返回值的类型。
template<typename T>  
T f();
 
struct S {int a;};
 
int a = 0;
decltype(auto) g1() {return s.a;}
decltype(auto) g2() {return std::move(a);}
decltype(auto) g3() {return (a);}
decltype(auto) g4() {return (0);}
 
int main()
{
    int a = 0;
    decltype(auto) i1 = a;
    decltype(auto) i2 = std::move(a);
    decltype(auto) i3 = (s.a);
    decltype(auto) i4 = (0);
    f<decltype(i1)>();
    f<decltype(i2)>();
    f<decltype(i3)>();
    f<decltype(i4)>();
    f<decltype(g1())>();
    f<decltype(g2())>();
    f<decltype(g3())>();
    f<decltype(g4())>();
}
declval 从类型到值

最终目的仍然是decltype获得类型

  • decltype(declval<F>()(参数们))

    配合declval获得一个值,再根据这个值进一步推导类型。

  • 编译器凭空创建一个函数对象(尽管其可能并没有构造函数)

  • declval只能用在不求值的表达式(编译期的语法糖)

取别名typedef

使用typedef取别名:

  • 未来需要多次使用该类型。
  • 甚至可以使用匿名结构体。

integral_constant

对于integral_constant来说,获取其中封装的类型 ::value_type

// 普通函数体中
typedef std::integral_constant<int, 10> int_10;
typedef typename int_10::value_type value_type; // int
value_type mVal = 10;

// 在模板函数中
// 加了typename一定没错,不加typename可能有错。
template<typename T>
void function(
	typename T::value_type val_1,
	// 或者
	T::value_type val_2) {}

模板函数中的具体规则:如果包含间接包含模板参数时,需要加typename。

因为编译期对这玩意做不到区分

  • 到底是某个类里的 名字为type的成员变量 typedef 函数指针…
  • 还是 type_trait声明的类型的指针,

所以需要typename进行特殊化。

模板函数返回值

invoke_result_t & declval

只知道函数的模板类型,没有函数对象。希望得到其返回值类型。

要求:函数存在没有参数的重载。

  • std::invoke_result_t<F>

    • 表示空调用后的返回类型
    • 缺点:参数列表可能不为空
  • decltype(f(1, 2, 3))

    • 缺点:可能不清楚合法的参数列表
  • decltype(declval<F>()())

    • decltype 配合 declval

    • 编译器凭空创建一个函数对象(尽管其可能并没有构造函数)

      declval 只能用在不求值的表达式(编译期的语法糖)

判断类型

common_type

返回参数列表中的参数都可以转换成的类型(有些拗口)。

// 返回值为double
typedef std::common_type<int, float, double>::type testType1;
std::is_same<double, testType1>::value; // true

typedef std::common_type<char, std::string>::type testType2;      // 报错,无法互相转换

可能的实现:

is_same

小技巧:忽略if分支

constexpr解决编译期出错 std::is_same_v 会在编译器略过错误分支,跳过else的非法命令 解决编译失败。

::value

不同于,这里的value就是类型是否一样。

在模板里可以通过std::is_same判断模板的类型,从而实现对不同类型的区别对待

is_same_v

std::is_same_v 判断类型是否相同。默认是decay的。

否则 会认为const, &, &&, 数组和指针不同,例如 vector的operator[]()返回的就是引用。需要使用std::decay_t<decltype(t)>

int main() {
    using namespace std;
    cout << is_integral<int>::value << endl; //int属于整型,所以输出1
    cout << is_integral<double>::value << endl; //double不属于整型,所以输出0
    cout << is_integral_v<int> << endl; //这是更好的写法,输出1
    cout << is_integral_v<double> << endl; //这是更好的写法,输出0
}

is_convertible

用于检查是否可以将任何数据类型A隐式转换为任何数据类型B。返回布尔值true或false。

template< class From, class To >
struct is_convertible;

template< class From, class To >
struct is_nothrow_convertible;


is_convertible <A*, B*>::value;
// 采用A和B两种数据类型
// - A  代表要转换的参数。
// - B  代表参数A隐式转换的参数。
// 代表 A 能否隐式转换到 B
// 不确定,同时代表 B 能否显式转换到 A?

is_convertible 继承自integral_constant,

  • 如果能转换, is_convertible<>::value 萃取到 true;
  • 如果不能转换,萃取到false。
// Derived子类转Base基类
bool BtoA = is_convertible<Derived*, Base*>::value; // true
bool InttoConst = is_convertible<int, const int>::value;	// true

类型特征is_xx

std::is_integral_v 是否是int类型。还有很多很多别的类模板,和is_integral类似

实现

std::is_xxx的实现基于模板特化

  • 当参数类型为xxx
    • std::is_xxx模板被特化,
    • value成员被设为true
  • 否则,继承自std::integral_constant
    • value成员为false

下面列举一些可能的实现。

template<class T>
struct is_fundamental
    : std::integral_constant<
        bool,
        std::is_arithmetic<T>::value ||
        std::is_void<T>::value ||
        std::is_same<std::nullptr_t, typename std::remove_cv<T>::type>::value
        // you can also use 'std::is_null_pointer<T>::value' instead in C++14
> {};
// Note: this implementation uses C++20 facilities
template<class T>
struct is_integral : std::bool_constant<
    requires (T t, T* p, void (*f)(T)) // T* parameter excludes reference types
    {
        reinterpret_cast<T>(t); // Exclude class types
        f(0); // Exclude enumeration types
        p + t; // Exclude everything not yet excluded but integral types
    }> {};
template<class T>
struct is_floating_point
     : std::integral_constant<
         bool,
         // Note: standard floating-point types
         std::is_same<float, typename std::remove_cv<T>::type>::value
         || std::is_same<double, typename std::remove_cv<T>::type>::value
         || std::is_same<long double, typename std::remove_cv<T>::type>::value
         // Note: extended floating-point types (C++23, if supported)
         || std::is_same<std::float16_t, typename std::remove_cv<T>::type>::value
         || std::is_same<std::float32_t, typename std::remove_cv<T>::type>::value
         || std::is_same<std::float64_t, typename std::remove_cv<T>::type>::value
         || std::is_same<std::float128_t, typename std::remove_cv<T>::type>::value
         || std::is_same<std::bfloat16_t, typename std::remove_cv<T>::type>::value
     > {};

std::is_enum 的实现通常依赖于编译器提供的内部特性来判断类型是否为枚举类型。

is_xx_v

在C++17及以后的版本中,同样可以使用带有_v后缀的变量模板。

std::is_xxx<mtype>::value等价于std::is_xxx_v<mtype>

这个类型特征的实现同样基于模板特化。仅当参数类型为std::nullptr_t时,std::is_null_pointervalue成员被特化为true

std::is_void<void>::value       // true
std::is_void<int>::value        // false


std::is_void_v<void>          // true
std::is_void_v<int>           // false

表格概述了C++中的基本类型类别(Primary Type Categories)以及对应的类型特征,包括它们在<type_traits>头文件中的定义和各自的作用:

类型特征C++版本描述
std::is_voidC++11检查类型是否为void
std::is_null_pointerC++14检查类型是否为std::nullptr_t
std::is_integralC++11检查类型是否为整型
std::is_floating_pointC++11检查类型是否为浮点型
std::is_arrayC++11检查类型是否为数组类型
std::is_enumC++11检查类型是否为枚举类型
std::is_unionC++11检查类型是否为联合体
std::is_classC++11检查类型是否为非联合类类型
std::is_functionC++11检查类型是否为函数类型
std::is_pointerC++11检查类型是否为指针类型
std::is_lvalue_referenceC++11检查类型是否为左值引用
std::is_rvalue_referenceC++11检查类型是否为右值引用
std::is_member_object_pointerC++11检查类型是否为指向非静态成员对象的指针
std::is_member_function_pointerC++11检查类型是否为指向非静态成员函数的指针

这个表格提供了一个简洁的总览,可以帮助理解和使用C++中的这些基本类型特征。这些特征是模板元编程中的重要工具,它们允许在编译时进行类型检查和推导,从而使得代码更加通用、灵活且类型安全。

子集关系

类型检查的过程属于编译时计算,不会增加运行时的开销。

通过编译时模板特化和SFINAE(Substitution Failure Is Not An Error,替换失败不是错误)技术实现的。SFINAE的原理是,在模板实例化过程中,如果某个替换导致编译错误,这个错误会被忽略,编译器会寻找其他可能的模板特化或重载。

这些子集关系在 C++ 的类型特征中非常重要,特别是在模板元编程和类型约束方面,它们帮助确保类型的正确使用和函数的适当重载。

基础类型

基础类型 std::is_fundamental 包括

  • 算术类型 std::is_arithmetic:专指涉及数值计算的类型

    在编写一个只接受数值类型参数的模板函数时,可以使用std::is_arithmetic来静态断言传入的类型符合要求

    • 整数类型 std::is_integral,int, char, long 等
    • 浮点类型 std::is_floating_point,float, double, long double等
  • void 类型 std::is_void

复合类型 std::is_compound 包括

基本类型的反义词,复合类型是由基本类型构造出来的类型。在C++中,任何类型要么是基本类型,要么是复合类型。

  • 类类型 std::is_class
  • 枚举类型 std::is_enum
  • 联合类型 std::is_union
  • 数组类型 std::is_array
  • 函数类型 std::is_functio
  • 指针类型 std::is_pointer

标量类型std::is_scalar包括

  • 算术类型 std::is_arithmetic
  • 枚举类型 std::is_enum
  • 指针类型 std::is_pointer
  • 成员指针 std::is_member_pointer
  • 空指针nullptr_t std::is_null_pointer

对象类型

对象类型 std::is_object,包括

在C++中是指拥有固定大小和存储的任何非函数类型

类型特征被用来判断一个类型是否为对象类型。

  • 标量类型、
  • 数组、
  • 类类型等。
C++17

表新的类型特征列出了 C++17引入的新类型特征。

特征效果
is_aggregate是否是聚合体类型
is_swappable该类型是否能调用 swap()
is_nothrow_swappable该类型是否能调用 swap()并且该操作不会抛出异常
is_swappable_with<T1, T2>特定值类型的这两种类型是否能调用 swap()
is_nothrow_swappable_with<T1, T2>特定值类型的这两种类型是否能调用 swap()并且该操作不会抛出异常
has_unique_object_representations是否该类型的两个值相等的对象在内存中的表示也一样
is_invocable<T, Args…>该类型是否可以用 Args… 调用
is_nothrow_invocable<T, Args…>该类型是否可以用 Args… 调用,并且该操作不会抛出异常
is_invocable_r<RT, T, Args…>该类型是否可以用 Args… 调用并返回 RT 类型
is_nothrow_invocable_r<RT, T, Args…>该类型是否可以用 Args… 调用并返回 RT 类型且不会抛出异常
invoke_result<T, Args…>Args… 作为实参进行调用会返回的类型
conjunction<B…>对 bool特征 *B…*进行逻辑与运算
disjunction<B…>对 bool特征 *B…*进行逻辑或运算
negation对 bool特征 B 进行非运算
is_execution_policy是否是执行策略类型

工具: 打印真实类型

#pragma once

#include <typeinfo>
#include <type_traits>
#include <string>
#if (defined(__GNUC__) || defined(__clang__)) && __has_include(<cxxabi.h>)
#include <cxxabi.h>
#include <cstdlib>
#endif

namespace _cppdemangle_details {

static std::string cppdemangle(const char *name) {
#if (defined(__GNUC__) || defined(__clang__)) && __has_include(<cxxabi.h>)
    int status;
    char *p = abi::__cxa_demangle(name, 0, 0, &status);
    std::string s = p ? p : name;
    std::free(p);
#else
    std::string s = name;
#endif
    return s;
}

static std::string cppdemangle(std::type_info const &type) {
    return cppdemangle(type.name());
}

template <class T>
static std::string cppdemangle() {
    std::string s{cppdemangle(typeid(std::remove_cv_t<std::remove_reference_t<T>>))};
    if (std::is_const_v<std::remove_reference_t<T>>)
        s += " const";
    if (std::is_volatile_v<std::remove_reference_t<T>>)
        s += " volatile";
    if (std::is_lvalue_reference_v<T>)
        s += " &";
    if (std::is_rvalue_reference_v<T>)
        s += " &&";
    return s;
}

}

using _cppdemangle_details::cppdemangle;

// Usage:
//
// cppdemangle<int>()
// => "int"
//
// int i;
// cppdemangle<decltype(i)>()
// => "int"
//
// int i;
// cppdemangle<decltype(std::as_const(i))>()
// => "int const &"
;