C++(标准库):08---Type Trait和Type Utility(type_traits库)

一、前言

  • C++标准库几乎每样东西都以template为根基。为了更多地支持template编程,彼岸准哭提供了template通用工具,协助应用程序开发人员和程序库作者
  • Type Trait,由TR1引入,在C++11中被大幅度扩展,定义出因type而异的行为。它们可被用来针对type优化代码,以便提供特别能力
  • 其他工具如reference和function wrapper,也为编程带来若干帮助

二、Type Trait的目的

  • 目的:提供一种用来处理type属性的方法。它是个template,可在编译期根据一个或多个template实参产出一个type或value

演示案例1(std::is_pointer<>)

#include <type_traits>

template<typename T>
void foo(const T& val)
{
    if (std::is_pointer<T>::value) {
        std::cout << "foo() called for a pointer" << std::endl;
    }
    else {
        std::cout << "foo() called for a value" << std::endl;
    }
}
int main()
{
    int num;
    int *p = &num;
    
    foo(num);
    foo(p);
}
  • 运行结果如下:

  • 代码解释:
    • 这里的std::is_pointer<>属于一个traits类。如果传入的参数类型为指针类型,其返回std::true_type;如果传入的参数类型不是指针类型,其返回std::false_type。然后std::true_type::value或std::false_type::value返回相对应的true或false
    • 第一个foo()调用传入的参数为非指针类型

演示案例2(std::true_type、std::false_type)

  • 我们修改演示案例1中的foo()函数,让其打印传入的元素的值,于是设计了下面的代码:
    • 但是这是错误的,编译不通过
    • 假设val不是指针类型,而代码中却有*val的操作,这显然是错误的
template<typename T>
void foo(const T& val)
{
    std::cout << (std::is_pointer<T>::value ? *val : val) << std::endl;
}
  • 如果想要达到上面的目的,可以借助std::true_type和std::false_type来完成。代码如下:
#include <type_traits>

//如果是指针,调用这个
template<typename T>
void foo_impl(const T& val,std::true_type)
{
    std::cout << "foo() called for a pointer:" << *val << std::endl;
}
//如果不是指针,调用这个
template<typename T>
void foo_impl(const T& val, std::false_type)
{
    std::cout << "foo() called for a value:" << val << std::endl;
}

template<typename T>
void foo(const T& val)
{
    foo_impl(val, std::is_pointer<T>());
}

int main()
{
    int num = 10;
    int *p = &num;

    foo(num);
    foo(p);
}

  • 直接调用foo_impl()函数也是可以的。例如:
int main()
{
    int num = 10;
    int *p = &num;

    foo_impl(num, std::is_pointer<int>());
    foo_impl(p, std::is_pointer<int*>());
}

演示案例3(std::is_integral<>,针对整数类型的弹性重载)

  • 例如现在我们有一批重载函数,一部分是针对于整数类型的,一部分是针对于浮点数类型的。例如:
void foo(short);
void foo(unsigned short);
void foo(int);

void foo(float);
void foo(double);
void foo(long double);
  • 上面的代码有很大的缺点:这样做重复工作很多,代码比较冗余,并且如果加入了新数据类型那么还需要重新定义新的foo()函数
  • 一种做法是使用trait机制提供的模板类,例如此处使用std::integral<>模板。定义的代码如下:
//针对于整数类型设计的
template<typename T>
void foo_impl(T val, std::true_type);

//针对于浮点数类型设计的
template<typename T>
void foo_impl(T val, std::false_type);

template<typename T>
void foo(T val)
{
    //通过is_integral萃取类型
    //如果T为整数类型,std::is_integral返回std::true_type;否则返回std::false_type
    foo_impl(val, std::is_integral<T>());
}

演示案例4(std::common_type<>,处理通用类型)

  • 假设我们有个函数来比较两个值的最小值,并将最小值进行返回,如果T1和T2的数据类型不一致,那么返回值该如何定义哪?

  • 我们可以借助std::common_type<>解决这个问题。例如:
    • 如果传入的两个实参都是int,或传入的都是long,或者传入的一个是int一个是long,那么std::common_type<>返回int,因此下面的min的返回值为int类型
    • 如果传入的参数一个是string而另一个是字符串字面常量,那么下面的min的返回值为std::string
template<typename T1, typename T2>
typename std::common_type<T1, T2>::type min(const T1& x, const T2& y);
  • 使用std::common_type<>的前提是,两个实参它们有共同的数据类型。前提是程序员自己保证的(看下面的实现原理)
  • std::common_type<>的实现原理如下:
    • 其内部使用?:运算符,直接返回T1的数据类型。因此上面不论min()的T2参数属于什么类型,其只返回T1所表示的数据类型
    • 其内部使用了一个std::declval<>模板,特属于trait的一种,其根据传入的类型提供一个值,但不去核算它(最终返回一个该值的rvalue reference)
    • 然后再使用decltype关键字导出表达式的类型

  • 通过上面的comm_type<>就可以找出一个共同类型,如果找不到,可以使用common_type<>的重载版本(这正是chrono程序库的作为,使它得以结合duration;详情见后面的chrono程序库介绍)

三、Type Trait的分类

  • Type trait大多数定义于<type_traits>头文件中,有些定义在别的头文件中(下面会注释)

①类型判断式

  • 下图列出了针对于所有类型都使用的trait:

  • 下图列出了针对于class类型都使用的trait

  • std::true_type、std::false_type:
    • 上面的类型的返回值是std::true_type或std::false_type
    • std::true_type和std::false_type都是std::integral_constant的特化,因此它们相应的value成员可以返回true或false。如下图所示:

  • 一些注意事项:
    • bool和所有character类型(char、char16_t、char32_t、wchar_t)都属于整数类型
    • std::nullptr_t为基础数据类型
    • 上面大部分都是单参数形式的,但并非全部都是
    • 一个“指向const类型”的非常量pointer或reference,其本身并不是一个常量(见下面演示案例)
    • 用以检验copy和move语义的那些trait,只检验是否相应的表达式为可能。例如,一个“带有copy构造函数(接受常量实参)但没有move构造函数”的类型,仍然是move constructible
    • is_nothrow..type trait特别被用来阐述noexcept异常声明
  • 下面是is_const<>的演示案例:
std::cout << boolalpha;
std::cout << "is_const<int>::value                 " << is_const<int>::value << std::endl;
std::cout << "is_const<const volatile int>::value  " << is_const<const volatile int>::value << std::endl;
std::cout << "is_const<int* const>::value          " << is_const<int* const>::value << std::endl;
std::cout << "is_const<const int*>::value          " << is_const<const int*>::value << std::endl;
std::cout << "is_const<const int&>::value          " << is_const<const int&>::value << std::endl;
std::cout << "is_const<int[3]>::value              " << is_const<int[3]>::value << std::endl;
std::cout << "is_const<const int[3]>::value        " << is_const<const int[3]>::value << std::endl;
std::cout << "is_const<int[]>::value               " << is_const<int[]>::value << std::endl;
std::cout << "is_const<const int[]>::value         " << is_const<const int[]>::value << std::endl;

②用以检验类型关系的Trait

  • 下图列出的type trait可以检查类型之间的关系,包括检查class type提供了哪一种构造函数和哪一种赋值操作等等

  • is_assignable<>的使用注意事项:
    • 注意,基本数据类型(例如int)可以表现出lvalue或是rvalue,因此你不能够直接赋值,例如“42=77”,这是错误的。因此is_assignable<>第一个类型如果是一个nonclass类型,永远会获得false_type
    • 如果是class类型,以其寻常类型作为第一类型是可以的,因为存在一个有趣的旧规则:你可以调用“类型为class”的rvalue的成员函数。例如:
std::cout << boolalpha;
std::cout << "is_assignable<int,int>::value                 " << is_assignable<int, int>::value << std::endl;
std::cout << "is_assignable<int&,int>::value                " << is_assignable<int&, int>::value << std::endl;
std::cout << "is_assignable<int&&,int>::value               " << is_assignable<int&&, int>::value << std::endl;
std::cout << "is_assignable<long&,int>::value               " << is_assignable<long&, int>::value << std::endl;
std::cout << "is_assignable<int&,void*>::value              " << is_assignable<int&, void*>::value << std::endl;
std::cout << "is_assignable<void*,int>::value               " << is_assignable<void*, int>::value << std::endl;
std::cout << "is_assignable<const char*,std::string>::value " << is_assignable<const char*, std::string>::value << std::endl;
std::cout << "is_assignable<std::string,const char*>::value " << is_assignable<std::string, const char*>::value << std::endl;

  • 下面是is_constructible<>的演示案例:
std::cout << boolalpha;
std::cout << "is_constructible<int>::value                             " << is_constructible<int>::value << std::endl;
std::cout << "is_constructible<int,int>::value                         " << is_constructible<int, int>::value << std::endl;
std::cout << "is_constructible<long,int>::value                        " << is_constructible<long, int>::value << std::endl;
std::cout << "is_constructible<int,void*>::value                       " << is_constructible<int, void*>::value << std::endl;
std::cout << "is_constructible<void*,int>::value                       " << is_constructible<void*, int>::value << std::endl;
std::cout << "is_constructible<const char*,std::string>::value         " << is_constructible<const char*, std::string>::value << std::endl;
std::cout << "is_constructible<std::string,const char*>::value         " << is_constructible<std::string, const char*>::value << std::endl;
std::cout << "is_constructible<std::string,const char*,int,int>::value " << is_constructible<std::string, const char*, int, int>::value << std::endl;

  • std::use_allocator<>被定义在<memory>头文件中

③类型修饰符

  • 下图列出的trait允许你改动类型

  • 使用规则:
    • 如果想要为某一类型添加一个属性,前提是该属性尚未存在
    • 如果想要为某一类型移除一个属性,前提是该属性已经存在
  • 下面是一些演示案例:

  • 类型const int&可被降级或扩展。例如:

  • 一些注意事项:
    • 一个“指向某常量类型”的reference本身并不是常量,所以你不可以移除其常量性
    • add_pointer<>必然包含使用remove_reference<>
    • 然后make_signed<>和make_unsigned<>要求实参若非整数类型就必须是枚举类型,bool除外,所以如果你传入reference会导致不明确行为
  • add_value_reference<>把一个rvalue reference转换为一个lvalue reference,然而add_rvalue_reference<>并不能把一个lvalue reference转换为一个rvalue reference(类型保持不变)。因此,必须这么做才能将一个lvalue转换为一个rvalue reference:

④其他trait

  • 下图列出了其余所有type trait。它们用来查询特殊属性、检查类型关系、或提供更复杂的类型变换

  • decay<>允许你讲“以by value传入”的类型T转换为其相应类型。以此方式,它转换array和function类型称为pointer,把lvalue转换为rvalue——其中包括移除const和volatile
  • common_type<>为所有被传入的类型提供一个共同类型(它可以有1个、2个或更多个类型实参)
  • 下面是一些演示案例:

四、Reference Wrapper(外覆器)

  • 声明于<functional>中的一些类型:
    • std::reference_wrapper<>:可以将传值调用改为传reference调用
    • std::ref():可以将类型T隐式转换为T&
    • std::cref():可以将类型T隐式转换为const T&
  • 演示案例:
template<typename T>
void foo(T val) { val++; }

int main()
{
    int num1 = 1, num2 = 1;
	
    foo(num1);
    std::cout << "num1:" << num1 << std::endl;

    foo(std::ref(num2)); //改为T&调用
    std::cout << "num2:" << num2 << std::endl;
}

  • 这些特性被C++标准库运用于各个地方,例如:
    • make_pair()用此特性,于是能够创建一个pair<> of references
    • make_tuple()用此特性,于是能够创建一个tuple<> of references
    • Binder用此特性,于是能够绑定reference
    • Thread用此特性,于是能够以by reference形式传递实参
  • 注意事项:class reference_wrapper使你得以使用reference作为最高级对象,例如作为array或STL容器的元素类型(演示案例参阅:https://blog.csdn.net/qq_41453285/article/details/105485576

五、Function Type Wrapper(外覆器)

  • std::function<>:
  • 演示案例:
int func(int x, int y)
{
    return x + y;
}

int main()
{
    std::vector<std::function<void(int, int)>> tasks;
    tasks.push_back(func);
    tasks.push_back([](int x, int y) { return x + y; });

    for (std::function<void(int, int)> f : tasks) {
        f(33, 66);
    }
}
  • 如果使用member function,那么必须将“调用它们”的那个对象作为参数1进行传递。例如:
class C {
public:
    void memfunc(int x, int y) {}
};

int main()
{
    std::function<void(const C&, int, int)> mf;
    mf = &C::memfunc;
    mf(C(), 42, 77);
}
  • 这个东西的另一个应用:声明某个函数返回一个lambda(详情见《C++标准库》P31)
  • 注意:执行一个函数调用,却没有标的物可调用,将会抛出std::bad_function_call异常。例如:

发布了1594 篇原创文章 · 获赞 1190 · 访问量 57万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/105454812
今日推荐