文章目录
类型特征是 C++ 模板元编程中使用的一种巧妙技术,它使您能够检查和转换类型的属性。
例如,给定一个泛型类型T
——它可以是int
、或任何你想要的——带有类型特征,你可以问编译器一些问题:它是一个整数吗bool
?std::vector
它是一个函数吗?是指针吗?或者也许是一堂课?它有析构函数吗?可以复制吗?它会抛出异常吗?… 等等。这在条件编译中非常有用,您可以指示编译器根据输入的类型选择正确的路径。我们很快就会看到一个例子。
类型特征也可以对类型应用一些转换。例如,给定T
,您可以添加/删除说明const
符、引用或指针,或者将其转换为有符号/无符号类型以及许多其他疯狂的操作。在编写使用模板的库时非常方便。
这些技术的美妙之处在于,一切都发生在编译时,没有运行时的惩罚:毕竟它是模板元编程。我假设您对本文其余部分的 C++ 模板有所了解。如果您不这样做,本指南是一个很好的介绍。
1 什么是类型特征?
类型特征是一个简单的模板结构,它包含一个成员常量,它又包含类型特征提出的问题或它执行的转换的答案。例如,让我们看一下std::is_floating_point
C++ 标准库在``标头中定义的众多类型特征之一:
模板<类型名 T >
结构 is_floating_point ;
这个类型特征告诉一个类型是否T
是浮点数。成员常量——value
要求提出问题的类型特征——将被设置为true
或false
根据作为模板参数传入的类型。
另一方面,例如std::remove_reference
,一个类型特征会改变T
它在输入中的类型:
模板<类型名 T >
结构 移除参考;
这种类型特征基本上变成T&
了T
. 成员常量——type
为那些修改类型的类型特征调用——包含转换的结果。
2 如何使用类型特征?
只需使用您想要的类型实例化模板结构,然后检查其成员常量并采取相应措施。例如,假设您只想打印出一个类型是否为浮点数:
#include <iostream>
#include <type_traits>
类 类{};
主函数 ()
{
std::cout << std::is_floating_point <类> :: value << '\n' ;
std::cout << std::is_floating_point < float > :: value << '\n' ;
std::cout << std::is_floating_point < int > :: value << '\n' ;
}
该程序将输出:
0
1
0
2.1 它是如何工作的?
在上面的代码片段中,您将三种不同的类型传递给模板结构std::is_floating_point
:自定义Class
类型、afloat
和 an int
。与任何常规模板一样,编译器将在后台为您生成三个不同的结构:
结构 is_floating_point_Class {
静态 常量 布尔 值 = 假;
};
结构 is_floating_point_float {
静态 常量 布尔 值 = 真;
};
结构 is_floating_point_int {
静态 常量 布尔 值 = 假;
};
此时,只需读取value
编译器创建的结构中的成员即可。作为静态的,您必须使用语法访问成员常量::
。请记住,这是模板元编程,所以一切都发生在编译时。
3 类型特征在行动,第 1 部分:条件编译
现在我们已经掌握了类型特征背后的想法,让我们尝试在一些现实世界的场景中使用它们。假设您有两个用于同一算法的函数:一个适用于有符号整数,另一个对无符号整数进行了超级优化。您希望编译器在int
传入 an 时选择有符号的,而unsigned int
在使用优化的情况下选择无符号的。这就是我之前提到的条件编译。
对于这项任务,我将使用三个工具:
- C++17
if constexpr
语法:if
在编译时起作用的语句; - C++11
static_assert
函数,顾名思义,如果条件不满足,则在编译时触发断言; - 两个不言自明的类型特征:
std::is_signed
和std::is_unsigned
。
代码如下所示:
void algorithm_signed ( int i ) { /*...*/ }
void algorithm_unsigned ( unsigned u ) { /*...*/ }
模板 <类型名 T >
无效 算法( T t )
{
if constexpr ( std::is_signed < T > :: value )
algorithm_signed ( t );
别的
if constexpr ( std::is_unsigned < T > :: value )
algorithm_unsigned ( t );
别的
static_assert ( std::is_signed < T > :: value || std::is_unsigned < T > :: value , "必须有符号或无符号!" );
}
换句话说,模板函数algorithm
充当调度程序:实例化时,编译器将根据T
传入的类型抓取正确的函数。如果签名,algorithm_signed
将被包含;如果未签名,algorithm_unsigned
则将被包含在内。最后,如果类型不符合条件,则抛出静态断言(即构建错误)。
一些使用示例:
算法(3); // T 是 int,包括 algorithm_signed()
无符号 x = 3 ;
算法( x ); // T 是无符号整数,包括 algorithm_unsigned()
算法(“你好”);// T 是字符串,构建错误!
4 类型特征在行动,第 2 部分:改变类型
类型特征也用于将转换应用于类型。这种魔法的典型用法来自C++ 标准库和:将类型转换为右值引用的实用函数。这是为移动语义铺平道路的重要操作。std::move
T``T&&
在内部,std::move
使用类型特征从输入中的类型中删除(如果有的话)并返回带有附加的*清理。*一个可能的实现:std::remove_reference
&
T``&&
模板 <类型名 T >
typename remove_reference < T > :: type && move ( T && arg )
{
return static_cast < typename remove_reference < T > :: type &&> ( arg );
}
像这样的转换在整个标准库中很普遍,通常用于优化函数参数在嵌套模板函数调用中的流动方式。总而言之,这些类型特征中的一些对于普通的 C++ 项目很少有用,除非您正在编写库或执行一些巧妙的元编程技巧。
5美化型特征
阅读代码::value
中::type
的任何地方都是令人困惑的。幸运的是,C++14 和更高版本引入了简化的语法,这要归功于一些分别以_v
和结尾的辅助别名_t
。例如:
std::is_signed < T > :: value ; /* ---> */ std::is_signed_v < T > ;
std::remove_const < T > :: type ; /* ---> */ std::remove_const_t < T > ;
这些帮助器存在于所有查询类型或对其应用转换的类型特征中。
6更多类型特征琐事和进一步阅读
类型特征是许多 C++ 特性的基础,并且一如既往,在本文中我几乎没有触及它们的表面。以下是未来值得更多爱的附加主题列表。
6.1类型特征知识的来源
类型特征如何知道类型?例如,它如何推断出对于std::is_signed_v<T>
a 是正确的int
?大多数基本类型特征是模板元编程技巧、SFINAE、标签调度和其他来自 C++ 黑暗角落的技术的结果。
一些类型特征需要额外的帮助。例如,std::is_abstract
类型特征——它告诉一个类型是否是一个抽象类——不能单独使用模板元编程来生成。出于这个原因,在标准库上工作的开发人员使用了内在函数:编译器提供的特殊内置函数,可以更深入地了解所讨论的类型,这要归功于编译器对其输入程序的深入了解。
6.2 类型特征和概念
概念是 C++20 中的一个重要补充:一种对模板函数或类可以接受的类型进行约束的优雅而富有表现力的方式。例如,在上面的条件编译示例中,我可以使用概念而不是触发最后的静态断言。毫不奇怪,概念基于标准库中定义的众多类型特征。有关概念的更多信息在这里。
6.3 类型特征提供自省
自省是程序检查对象的类型或属性的能力。例如,通过自省,您可以询问对象是否具有特定的成员函数以便调用它。
C++ 无法在运行时进行自省,但正如我们在本文中看到的,由于类型特征,它在编译时做得很好。在前面的示例中检查是否已签名时,我们肯定使用了编译时自省。T
另一方面,反射是指程序观察和改变其自身结构或行为的能力。目前 C++ 中还没有这样的东西,但是一些编程艺术家正在通过利用类型特征与模板元编程相结合的力量来开发诸如magic_get之类的疯狂库。还有一些建议在现代 C++ 中包含反射,在此处和此处起草。时间会告诉我们…