目录
1. 特征技术概述(Traits)
C++ 特征技术是一种强大的机制,它允许你在编译时获取类型的信息。它使你能够编写能够适应不同类型的泛型代码,而无需运行时开销。
您可以将特征视为具有多种结果的一个类型函数或一组类型函数。标准库提供了allocator_traits,char_traits,iterator_traits,regex_traits,pointer_traits。此外,它还提供了 time_traits和 type_traits。
关键概念为:
类型特征(Type Traits)——提供关于类型的属性。
策略(Policy)——将接口定义为对其他类的服务的类或类模板。
模板逾编程(Template Metaprogramming)——使用模板在编译时执行计算(以避免运行时计算,提升运行时性能)。
1.1 特征技术的目的
以下是关键方面的细分:
(1) 类型自省:
特征技术提供了一种(编译时)查询类型属性的方法,例如某个类型是整数、指针还是类。
(2) 编译时自定义:
基于从特征获取的信息,您可以在编译时自定义代码的行为。这允许针对不同类型的代码路径进行优化。
(3) 泛型编程:
特征对于编写可处理多种类型的泛型算法和数据结构至关重要。
(4) 扩展功能:
特征技术可以通过提供类型的信息来扩展和调整现有代码。
1.2 特征技术实现方式
(1) 模板类:
特征通常以模板类的形式实现。它们将类型作为模板参数。
(2) 静态成员:
特征类通常包含静态成员常量或成员类型,用于提供有关类型参数的信息。
(3) 特化:
您可以针对特定类型特化特征类,以提供不同的行为或信息。
(4) 编译时估算:
特征在编译时进行求值,因此没有运行时开销。
1.3 特征技术常见用例
(1) 类型检查:
诸如 std::is_integral,std::is_pointer 和 std::is_class 之类的特征允许您检查类型的属性。
(2) 类型转换:
诸如 std::remove_const,std::add_pointer 和 std::remove_reference 之类的特征允许您修改类型。
(3) 算法优化:
通过使用特征,您可以编写能够针对特定类型使用优化的泛型实现的算法,即针对特定类型的模板特化。
(4) 库开发:
特征在 C++ 标准库中被广泛使用,以提供泛型功能。
2. 应用举例
不同的代码片段结构基本相同,但在细节上存在差异,这种情况并不少见。理想情况下,我们能够重用这些结构,并剔除这些差异。在 C 语言中,这可以通过使用函数指针来实现,例如 C 标准库中的 qsort 函数,或者在 C++ 中使用虚函数。遗憾的是,这与编译时已知的运行时情况不同,并且会产生运行时开销。
C++ 引入了泛型编程,使用模板,消除了运行时绑定的需求,但乍一看,这仍然像是一种妥协,毕竟,相同的算法并非适用于所有数据结构。对链表进行排序与对数组进行排序不同。排序数据的搜索速度比未排序数据快得多。
C++ 特征是模板编程的一个特例,通常只关注特定类型的特性,例如静态属性和静态函数。它是 C++ 模板编程最基本的构建块之一。
C 和 C++ 程序员都应该熟悉 limits.h 和 float.h,它们用于确定整数和浮点类型的各种属性。
大多数 C++ 程序员都熟悉 std::numeric_limits ,乍一看,它只是提供了相同的服务,只是实现方式不同。通过仔细研究numeric_limits,我们发现了 trait 的第一个优点:一致的接口。
使用 float.h 和 limits.h 时,你必须记住类型前缀和特征,例如, DBL_MAX 包含 double 数据类型的“最大值”特征。通过使用像numeric_limits这样的特征类,类型会成为名称的一部分,因此 double 的最大值将变为numeric_limits<double>::max() ,更重要的是,你不需要知道需要使用哪种类型。例如,以这个简单的模板函数(改编自 [ Veldhuizen ])为例,它返回数组中的最大值:
template< class T >
T findMax(const T const* data,
const size_t const numItems) {
// Obtain the minimum value for type T
T largest =
std::numeric_limits< T >::min();
for (unsigned int i = 0; i < numItems; ++i)
if (data[i] > largest)
largest = data[i];
return largest;
}
注意numeric_limits的用法。正如您所见,与C风格的limits.h习语一样,您必须知道类型,而使用C++ 的traits习语,只有编译器需要知道类型。不仅如此,numeric_limits与大多数traits一样,只需通过创建模板的特化版本即可扩展为包含您自己的自定义类型(例如定点或任意精度算术类)。
我们不讨论numeric_limits,它只是一个trait实际应用的示例,我们介绍如何创建您自己的traits类。
3. 创建自己的特征类
C++ 特征可用于统一不同类型的低级函数接口,从而简化高级模板编程。
例如,如果我们想为不同的数值类型(包括 int32_t,int64_t,float 和 double)创建函数,例如 greater_than_half_maximum,并使用该类型的最大值,那么如果不使用 特征技术,就很难充分利用模板编程的优势。
template <typename T>
bool greater_than_half_maximum(T value)
{
return false;
}
template <>
bool greater_than_half_maximum<int32_t>(int32_t value)
{
// 2^31 - 1
if (value > 2147483647 / 2)
{
return true;
}
else
{
return false;
}
}
template <>
bool greater_than_half_maximum<int64_t>(int64_t value)
{
// 2^63 - 1
if (value > 9223372036854775807 / 2)
{
return true;
}
else
{
return false;
}
}
template <>
bool greater_than_half_maximum<float>(float value)
{
// FLT_MAX defined in cfloat
if (value > FLT_MAX / 2)
{
return true;
}
else
{
return false;
}
}
template <>
bool greater_than_half_maximum<double>(double value)
{
// DBL_MAX defined in cfloat
if (value > DBL_MAX / 2)
{
return true;
}
else
{
return false;
}
}
// Even more specializations for different types.
// ...
从上面的例子我们可以看出,即使我们想使用模板编程,而不使用特征,不同类型的特化实现也是不可避免的。
std::numeric_limits 是 C++ 标准库中的一个类型特征,它定义了不同内置类型的最大值。通过使用 std::numeric_limits<T>::max(),我们无需针对特定用例为不同类型创建特化。
#include <limits>
template <typename T>
bool greater_than_half_maximum(T value)
{
if (value > std::numeric_limits<T>::max() / 2)
{
return true;
}
else
{
return false;
}
}
对于自定义数值类型,例如 NVIDIA __half,我们可以为 std::numeric_limits 创建特化,而不是为 greater_than_half_maximum 创建特化。
namespace std
{
template <>
class numeric_limits<__half>
{
public:
constexpr static __half max()
{
constexpr uint16_t const half_max_bits{ 0x7BFF };
__half const half_max{ *reinterpret_cast<__half const*>(&half_max_bits) };
return half_max;
};
};
} // namespace std
当然,有人可能会说,对于不同的类型,我们必须创建不同的类型特征,这同样需要大量的工作。然而,由于低级类型特征通常比高级特化更具可复用性,因此,相比于高级模板特化,在创建低级类型特征上投入精力更有价值。此外,由于高级模板特化通常比类型特征需要更多行实现,因此创建低级类型特征所需的工作量比高级模板特化要少。
3. C++ 特征对模板逾编程的贡献
C++ 类型特征对于模板逾编程至关重要。我们将了解它如何用于函数模板逾编程和类模板逾编程。
3.1 函数模板逾编程
当类型特征与 std::enable_if 一起使用时,我们可以针对不同类型特化函数实现细节,正如我之前的博客文章“使用 Enable If 进行 C++ 模板特化”中所述。
3.2 类模板逾编程
当类型特征与 std::conditional_t 一起使用时,我们可以针对具有不同类型的成员变量且无法简单模板化的类,特化其实现细节。
例如,C++ <random> 库有两个用于生成均匀分布随机数的函数,包括用于生成整数值的std::uniform_int_distribution和用于生成实数值的 std::uniform_real_distribution。
以下实现使用类型特征 std::is_integral<T>::value 统一了这两个随机数生成函数 std::uniform_int_distribution 和 std::uniform_real_distribution。
#include <iostream>
#include <iterator>
#include <random>
#include <vector>
template <typename T>
class UniformDist
{
public:
UniformDist(T a, T b) : m_min{ a }, m_max{ b }, m_uniform_dist{ a, b } {}
T operator()(std::mt19937& random_engine)
{
return m_uniform_dist(random_engine);
}
private:
T const m_min;
T const m_max;
using dist_t = std::conditional_t<std::is_integral<T>::value,
std::uniform_int_distribution<T>,
std::uniform_real_distribution<T>>;
dist_t m_uniform_dist;
};
template <typename T>
std::vector<T> create_random_vector(size_t n, T a, T b,
std::mt19937& random_engine)
{
UniformDist<T> uniform_dist{ a, b };
std::vector<T> vec(n, 0.0);
for (size_t i{ 0 }; i < n; ++i)
{
vec[i] = uniform_dist(random_engine);
}
return vec;
}
int main()
{
size_t const n{ 8 };
unsigned int const seed{ 0U };
std::mt19937 random_engine{ seed };
std::vector<double> const random_vector_double{
create_random_vector(n, -16.0, 16.0, random_engine) };
std::cout << "Random Vector of Doubles: " << std::endl;
std::copy(random_vector_double.begin(), random_vector_double.end(),
std::ostream_iterator<double>(std::cout, " "));
std::cout << std::endl;
std::vector<int> const random_vector_int{
create_random_vector(n, -16, 16, random_engine) };
std::cout << "Random Vector of Integers: " << std::endl;
std::copy(random_vector_int.begin(), random_vector_int.end(),
std::ostream_iterator<int>(std::cout, " "));
std::cout << std::endl;
}