What is type traits
什么是Type traits?
Type traits are a clever technique used in C++ template metaprogramming that gives you the ability to inspect and transform the properties of types.
Type traits通常被用在conditional compliation中(c++17引入),编译器来根据不同的输入类型,生成不同的代码。(后面举例)
比如,给定一个类型T,它可以是int,bool,std::vector或者任意类型,借助type traits,我们可以对编译器问各种问题:
- T是整型吗
- T是函数吗
- T是指针吗
- T是一个类吗
- T有析构函数吗
- T可以复制吗
- T会抛异常吗
- T有一个名字叫reserve的成员函数吗
- …
有了
type traits,在mordern c++中,可以取代SFINAE
Type traits还可以对类型做转换
比如给定类型T,我们可以做如下转换:
- 增加或者删除
const描述符 - 增加或者删除引用
- 增加或者删除指针
- 转换称为一个有符号数或者无符号数
- …
这个通常只会在写一个library的时候才会见到,绝大多数应用程序是很难见到类似用法的。
type traits只发生在编译器,没有任何运行期消耗,就是一个模板元编程的技巧。
What is a type trait?
本质上type traits就是一个模板类,这个模板类包含若干成员常量(member constant),这些常量:
- 要么是之前向编译器询问的问题的答案
- 要么是类型转换之后的结果
我们分别举例:
template<typename T>
struct is_floating_point;
std::is_floating_point是<type_traits>里的一个type traits,作用就是告知T是否是一个浮点数。我们可以看到它其实就是一个struct,里面有一个bool常量value,根据传入的类型不同,value成员会被设置为true或者false。
另一个用法如下
template<typename T>
struct remove_reference;
remove_reference是一个类型转换,将T&转换为T,同样它也是一个struct,里面有一个常量type就是转换之后的类型。
How do I use a type trait?
#include <iostream>
#include <type_traits>
class Class {};
int main() {
std::cout << std::is_floating_point<Class>::value << '\n';
std::cout << std::is_floating_point<float>::value << '\n';
std::cout << std::is_floating_point<int>::value << '\n';
}
运行之后打印结果就是
0
1
0
How does it work exactly?
编译器实际会根据type_traits传入的不同类型,会生成多个struct,比如对于上面的例子,总共传入了Class,float,int三个类型,编译器就会实际产生类似如下代码(注意其类型是static):
struct is_floating_point_Class {
static const bool value = false;
};
struct is_floating_point_float {
static const bool value = true;
};
struct is_floating_point_int {
static const bool value = false;
};
所以上面的代码实际就会转换成如下代码
int main() {
std::cout << std::is_floating_point_Class::value << '\n';
std::cout << std::is_floating_point_float::value << '\n';
std::cout << std::is_floating_point_int::value << '\n';
}
编译器根据传入的三个不同类,生成对应的结构体,每个结构体有一个成员变量value,我们只需要通过::获取其值就可以。再次说明这些都是发生在编译期,没有任何运行期开销。
Under the hood
真实是否真的是像上面所说?
我们在c++ insight运行类上面的代码,可以看到实际运行的代码如下:
int main() {
std::operator<<(std::cout.operator<<(std::integral_constant<bool, false>::value), '\n');
std::operator<<(std::cout.operator<<(std::integral_constant<bool, true>::value), '\n');
std::operator<<(std::cout.operator<<(std::integral_constant<bool, false>::value), '\n');
}
也就是is_floating_point实际被转换为了另一个type_traits (integral_constant),c++ reference中给出的一种可能实现如下
template< class T >
struct is_floating_point
: std::integral_constant<
bool,
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
> {};
How to use type traits
- Writing algorithms
- Restricting templates using enable_if
- Provide optimized versions of generic code for some types
conditional compilation
假设再写一个算法,这个算法对于有符号数和无符号数有不同处理,那么就可以利用type_traits
void algorithm_signed (int i) { /*...*/ }
void algorithm_unsigned(unsigned u) { /*...*/ }
template <typename T>
void algorithm(T t)
{
if constexpr(std::is_signed<T>::value)
algorithm_signed(t);
else
if constexpr (std::is_unsigned<T>::value)
algorithm_unsigned(t);
else
static_assert(std::is_signed<T>::value || std::is_unsigned<T>::value, "Must be signed or unsigned!");
}
这个模板函数algorithm就像一个分发器,当传入有符号数的时候调用的是algorithm_signed,当传入无符号数时则会调用algorithm_unsigned,而当既不是有符号数也不是无符号数时,则会直接在编译器报错。
algorithm(3); // T is int, include algorithm_signed()
unsigned x = 3;
algorithm(x); // T is unsigned int, include algorithm_unsigned()
algorithm("hello"); // T is string, build error!
altering types
类型转换其实在标准库中会非常常见,比如std::move本质上就是把一个类型T转换为右值引用T&&,实际上std::move本质上就是调用了std::remove_reference这个type traits。std::move实际的操作如下:
- 如果传入的类型
T带引用&则把它去掉(没有则不做任何操作),此时的类型为T' T'再转换为右值引用T'&&
一种可能实现如下:
template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
return static_cast<typename remove_reference<T>::type&&>(arg);
}
实际上std::move(t)返回的类型就是static_cast<typename std::remove_reference<T>::type&&>(t)
syntax sugar
在C++14中为了避免type traits中的各种::value或者::type,引入了两个语法糖_v和_t,如下所示
std::is_signed<T>::value; /* ---> */ std::is_signed_v<T>;
std::remove_const<T>::type; /* ---> */ std::remove_const_t<T>;
也就是要使用问编译器类型是否满足特定条件时使用_v,而要使用类型转换结果时使用_t,绝大多数<type_traits>中的type traits都有对应的语法糖。
Template argument deduction
这部分实际还和模板推导有关,等下次再回顾下。
https://en.cppreference.com/w/cpp/language/template_argument_deduction
#include <type_traits>
#include <iostream>
int func(...) { return 0; }
int func(float f) { return 1; }
template <typename T>
typename std::enable_if<std::is_integral<T>::value, int>::type
func(T val) { return 2; }
int main() {
// 0 2 1 1
std::cout << func(nullptr) << " ";
std::cout << func(2) << " ";
std::cout << func(2.f) << " ";
std::cout << func(2.0) << std::endl;
}
#include <type_traits>
#include <iostream>
int func(...) { return 0; }
int func(float f) { return 1; }
template <typename T>
typename std::enable_if<std::is_integral<T>::value, int>::type
func(T val) { return 2; }
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, int>::type
func(T val) { return 3; }
int main() {
// 0 2 1 3
std::cout << func(nullptr) << " ";
std::cout << func(2) << " ";
std::cout << func(2.f) << " ";
std::cout << func(2.0) << std::endl;
}