C++类型特征的用法(附带实例)
模板元编程是语言的一个强大特性,它使我们能够编写和重用适用于所有类型的通用代码。然而,在实践中,对于不同的类型,由于意图、语义正确性、性能或其他原因,通用代码通常需要以不同的方式工作,或者根本不工作。
例如,你可能希望针对 POD 类型和非 POD 类型实现不同的通用算法,或者希望仅用整型实例化函数模板。C++11 提供了一组类型特征(type traits)来帮助实现这一点。
类型特征基本上是提供其他类型信息的元类型。类型特征库包含一长串特征,它们用于获取类型属性(例如检查类型是否是整型或两个类型是否想同),也用于执行类型转换(例如删除 const 和 volatile 限定符或添加指向类型的指针)。
在本节中,我们将研究类型特征是什么以及它们是如何工作的。
1) 使用 enable_if 为类型定义前置条件,函数模板可以使用这些类型实例化:
2) 使用 static_assert 确保满足不变量:
3) 使用 std::conditional 在类型之间进行选择:
4) 使用 constexpr if 使编译器能够根据实例化模板的类型生成不同的代码:
为了方便起见,这里提供了一个简短的摘要:
类型特征是通过提供类模板和它的部分特化或完整特化来实现的。下面表示一些类型特征的概念实现:
1) is_void() 方法指示类型是否为 void。这使用了完整特化:
2) is_pointer() 方法指示类型是指向对象的指针还是指向函数的指针,这使用了部分特化:
因此,从 C++20 开始,更细粒度的 trivial 类型和标准布局类型概念是首选,这也意味着不应该再使用 std::is_pod,而应该分别使用 std::is_trivial 和 std::is_standard_layout。
例如,你可能希望针对 POD 类型和非 POD 类型实现不同的通用算法,或者希望仅用整型实例化函数模板。C++11 提供了一组类型特征(type traits)来帮助实现这一点。
类型特征基本上是提供其他类型信息的元类型。类型特征库包含一长串特征,它们用于获取类型属性(例如检查类型是否是整型或两个类型是否想同),也用于执行类型转换(例如删除 const 和 volatile 限定符或添加指向类型的指针)。
在本节中,我们将研究类型特征是什么以及它们是如何工作的。
C++类型特征的使用
C++11 引入的所有类型特征都能在 <type_traits> 头文件的 std 命名空间中找到。下面列出了使用类型特征来实现各种设计目标的各种情况:1) 使用 enable_if 为类型定义前置条件,函数模板可以使用这些类型实例化:
template <typename T,typename = typename std::enable_if_t<std::is_arithmetic_v<T> > > T multiply(T const t1, T const t2) { return t1 * t2; } auto v1 = multiply(42.0, 1.5); // OK auto v2 = multiply("42"s, "1.5"s); // error
2) 使用 static_assert 确保满足不变量:
template <typename T> struct pod_wrapper { static_assert(std::is_standard_layout_v<T> && std::is_trivial_v<T>, "Type is not a POD!"); T value; }; pod_wrapper<int> if 42 }; // OK pod_wrapper<std::string> s{ "42"s }; // error
3) 使用 std::conditional 在类型之间进行选择:
template <typename T> struct const_wrapper { typename std::conditional_t <std::is_const_v<T>, T, typename std::add_const_t<T>> const_type; }; static_assert(std::is_const_v<const_wrapper<int>::const_type>); static_assert(std::is_const_v<const_wrapper<int const>::const_type>);
4) 使用 constexpr if 使编译器能够根据实例化模板的类型生成不同的代码:
template <typename T> auto process(T arg) { if constexpr (std::is_same_v<T, bool>) return !arg; else if constexpr (std::is_integral_v<T>) return std::abs(arg); else return arg; } auto v1 = process(false); // v1 = true auto v2 = process(42); // v2 = -42 auto v3 = process(-42.0); // v3 = 42.0 auto v4 = process("42"s); // v4 = "42"
C++类型特征的工作原理
类型特征是提供关于类型的元信息或可用于修改类型的类。实际上有两种类型特征:- 提供关于类型、类型属性或类型关系的信息的类型特征(如 is_integer、is_arithmetic、is_array、is_enum、is_class、is_const、is_trivial、is_standard_layout、is_constructible、is_same 等),这些特征提供了一个称为 value 的 bool 常量成员。
- 修改类型属性的类型特征(如 add_const、remove_const、add_pointer、remove_pointer、make_signed、make_unsigned 等),这些特征提供了一个名为 type 的成员 typedef,它表示转换后的类型。
为了方便起见,这里提供了一个简短的摘要:
- 在第一个例子中,函数模板 multiply() 只允许用算术类型(即整型或浮点型)实例化,当使用不同类型的类型实例化时,enable_if 没有定义名为 type 的 typedef 成员,这会产生编译错误。
- 在第二个例子中,pod_wrapper 是一个类模板,它应该只使用 POD 类型进行实例化。如果使用非 POD 类型(要么是非 trivial 类型,要么是非标准布局类型),static_assert 声明将产生编译错误。
- 在第三个例子中,const_wrapper 是一个类模板,它提供了一个名为 const_type 的 typedef 成员,该成员表示一个常量限定类型。
- 在本例中,我们使用 std::conditional 于编译时在两种类型之间进行选择:如果类型形参 T 已经是常量类型,那么只选择 T;否则,使用 add_const 类型特征用 const 说明符限定类型。
- 在第四个例子中,process() 是包含一系列 if constexpr 分支的函数模板。根据类型的类别,在编译时使用各种类型特征(is_same_v、is_integer、is_floating_point)获取,编译器只选择一个分支放入生成的代码中,将其余的丢弃。因此,像 process(42) 这样的调用会产生如下函数模板的实例化代码:
int process(int arg) { return -arg; }
类型特征是通过提供类模板和它的部分特化或完整特化来实现的。下面表示一些类型特征的概念实现:
1) is_void() 方法指示类型是否为 void。这使用了完整特化:
template <typename T> struct is_void { static const bool value = false; }; template <> struct is_void<void> { static const bool value = true; };
2) is_pointer() 方法指示类型是指向对象的指针还是指向函数的指针,这使用了部分特化:
template <typename T> struct is_pointer { static const bool value = false; }; template <typename T> struct is_pointer<T*> { static const bool value = true; };需要注意的是,在 C++20 中,POD 类型的概念已被弃用,这也包括对 std::is_pod 类型特征的弃用。POD 类型是一种类型,它既是 trivial 类型(具有编译器提供的或显式默认的特殊成员,并占用连续的内存区域),又是标准布局类型(不包含语言特性的类,例如与 C语言不兼容的虚函数,并且所有成员具有相同的访问控制机制)。
因此,从 C++20 开始,更细粒度的 trivial 类型和标准布局类型概念是首选,这也意味着不应该再使用 std::is_pod,而应该分别使用 std::is_trivial 和 std::is_standard_layout。