变量模板

1. 基本语法

在 C++14 之前,我们只能定义函数模板类模板。C++14 引入了第三种模板变量模板它允许我们将变量定义为模板。这使得我们可以创建类型相关的常量值或变量,大大增强了模板元编程的能力。

变量模板的声明方式类似于函数模板或类模板,只是在开头使用了 template <...> 和随后的变量声明。

基本语法格式如下:

1
2
3
4
5
template<typename T>
constexpr T pi = T(3.1415926535897932385L);

template<typename T>
T special_value = T(42);

1.1 简单的变量模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

template<typename T>
constexpr T pi = T(3.1415926535897932385L);

int main()
{
// 实例化为 double 类型的 pi
double d = pi<double>;

// 实例化为 int 类型的 pi
int i = pi<int>;

cout << "d = " << d << ", i = " << i << endl;
return 0;
}

在上面的例子中,pi 是一个变量模板。当我们使用 pi<double> 时,编译器会实例化一个类型为 double 的变量;使用 pi<int> 时则实例化一个 int 变量。

1.2 类型相关的常量

numeric_limits 是 C++ 标准库中的一个类模板,位于 <limits> 头文件中。它用于查询各种算术类型(如整数、浮点数)的属性信息。

我们可以使用这个类的静态方法获取类型相关的常量,比如数字极限:

  • max() - 类型的最大值
  • min() - 类型的最小值
  • lowest() - 类型的最小值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <limits>
#include <iostream>

template<typename T>
constexpr T max_value = std::numeric_limits<T>::max();
template<typename T>
constexpr T min_value = std::numeric_limits<T>::min();

int main()
{
std::cout << "int max: " << max_value<int> << std::endl;
std::cout << "float max: " << max_value<float> << std::endl;
std::cout << "int min: " << min_value<int> << std::endl;
std::cout << "float min: " << min_value<float> << std::endl;
return 0;
}

1.3 默认参数

在 C++14 中使用变量模板的时候,也可以给它指定默认模板参数,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

// 变量模板带默认参数
template<typename T = double>
constexpr T pi = T(3.1415926535897932385L);

int main()
{
// 使用默认参数
std::cout << pi<> << std::endl; // 等价于 pi<double>
std::cout << pi<double> << std::endl; // 显式指定

// 指定不同类型
std::cout << pi<float> << std::endl;
std::cout << pi<long double> << std::endl;

return 0;
}

如果给变量模板指定了默认参数,以下这两种使用方法是等价的:

  • 使用默认类型 double:pi<>

  • 显式指定 double:pi<double>

关于给定的实际数据(例如上面示例中的3.1415926535897932385L)的类型转换也是有两种书写方式:

  • C 语言风格的类型转换,使用 T()

    1
    2
    template<typename T = double>
    constexpr T pi = T(3.1415926535897932385L);
  • 使用 C++ 风格的类型转换,使用static_cast<T>()

    1
    2
    template<typename T = double>
    constexpr T pi2 = static_cast<T>(3.1415926535897932385L);

2. 变量模板特化

变量模板特化允许我们为特定的模板参数提供定制化的实现。与类模板特化类似,变量模板特化也分为全特化偏特化两种形式。不论是哪种变量模板的特化一共分为以下几个步骤:

  1. 声明主模板
  2. 选择特化类型:全特化 / 偏特化
  3. 编写特化声明
    • 全特化template<>
    • 偏特化template<修改的参数列表>
  4. 确保特化可见性(放在使用之前)
  5. 测试所有特化路径

2.1 全特化

2.1.1 返回固定类型

1
2
3
// 步骤1:主模板
template<typename T>
constexpr const char* type_name = "unknown";
  • 模板参数为具体类型:例如intdoublestd::string
  • 返回类型固定:无论 T 是什么类型,都返回 const char*
  • 值不同:通过模板特化为不同的类型返回不同的字符串
  • 用途:获取类型的字符串表示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 步骤2:编写特化声明
template<>
constexpr const char* type_name<int> = "int";

template<>
constexpr const char* type_name<double> = "double";

template<>
constexpr const char* type_name<std::string> = "std::string";

// 步骤3:使用
int main()
{
std::cout << type_name<int> << std::endl; // 输出: int
std::cout << type_name<double> << std::endl; // 输出: double
std::cout << type_name<std::string> << std::endl; // 输出: std::string
std::cout << type_name<float> << std::endl; // 输出: unknown
return 0;
}
  • template<>:表示完全特化(没有剩余模板参数)
  • constexpr const char*:与主模板一致,返回特定类型
  • type_name<int>:特化的具体实例,模版参数为具体类型(如 int, double, const char*等)

2.1.2 返回类型依赖模板参数

  1. 声明主模版

    1
    2
    template<typename T>
    constexpr T default_value = T{};
    • template<typename T>:声明这是一个模板,T 是类型参数(例如intdoublestd::string等)
    • constexpr:C++11 引入,C++14 增强,表示这是编译期常量,如果程序需要可以自行添加
    • default_value:变量模板名
    • T{}:值初始化,对于内置类型初始化的默认值为零
  2. 编写特化声明

    1
    2
    3
    4
    5
    6
    7
    8
    template<>
    constexpr int default_value<int> = -1;

    template<>
    constexpr double default_value<double> = 0.0;

    template<>
    constexpr const char* default_value<const char*> = "default";
    • template<>:表示完全特化(没有剩余模板参数)
    • constexpr int:必须提为T供具体的特化的类型(如 int, double, const char*等)
    • default_value<int>:特化的具体实例
    • = -1:为int类型提供的特殊默认值

让我们扩展上面这个例子,展示更多的 C++14 特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <type_traits>

// C++14 变量模板
template<typename T>
constexpr T default_value = T{};

// 特化(C++14 允许更灵活的特化)
template<>
constexpr int default_value<int> = 666;

template<>
constexpr double default_value<double> = 3.14;

template<>
constexpr const char* default_value<const char*> = "Hello, C++14!";

// C++14 对 auto 的增强(返回类型推导)
template<typename T>
constexpr auto get_default()
{
return default_value<T>; // 自动推导返回类型
}

// C++14 泛型 lambda
auto print_default = [](auto type_id)
{
using T = decltype(type_id);
std::cout << "get_default value: " << get_default<T>() << std::endl;
std::cout << "default_value: " << default_value<T> << std::endl;
};

int main()
{
// 使用变量模板
std::cout << default_value<int> << std::endl;

// C++14 的数字分隔符(提高可读性)
constexpr long long big_number = 1'000'000'000;

// 使用泛型 lambda
print_default(0); // int: 666
print_default(0.0); // double: 3.14
print_default("hello"); // const char*: Hello, C++14!
print_default(true); // bool: 0

return 0;
}

对应上面的代码有一个小细节需要大家理解和掌握。有这样一行代码:

1
constexpr const char* default_value<const char*> = "default";
  • constexpr:这个变量本身(指针变量)是编译期常量(指针值在编译期确定且不可变)
  • const char*:这是类型,表示指向常量字符的指针

但是这里有个特殊情况!由于有 constexpr 修饰,其实指针本身也变成常量了。所以可以得到如下结论:

1
constexpr const char* ptr = "Hello";

等价于

1
const char* const ptr = "Hello";  // 实际上 constexpr 也使得指针本身是 const

因为 constexpr 对象默认是 const 的(C++标准规定)。所以实际上:

  • 指针本身不可变(因为有 constexpr
  • 指向的内容也不可变(因为指向 const char

2.2 偏特化

在 C++14 中,偏特化 是模板编程中的一个核心概念,它允许我们为模板参数的特定模式特定子集提供自定义的实现,而不是使用通用的主模板。

对于变量模板而言,偏特化意味着:针对某些特定的类型条件(例如指针、数组、特定类型的组合),重新定义该变量的值。

为了详细说明变量模板偏特化的用途,我们将通过几个具体的场景进行演示。

2.2.1 类型特征检查

这是 C++14 引入变量模板最主要的原因之一:为了简化 C++11 中繁琐的 std::some_trait<T>::value 写法,变为 std::some_trait_v<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

// 主模板:默认情况下,假设类型 T 不是指针
template<typename T>
constexpr bool is_pointer_trait = false;

// 偏特化:针对任何类型的指针 T*
template<typename T>
constexpr bool is_pointer_trait<T*> = true;

// 偏特化:针对指向成员的指针 (例如 int MyClass::*)
template<typename T, typename Class>
constexpr bool is_pointer_trait<T Class::*> = true;

int main()
{
std::cout << std::boolalpha;

// 匹配主模板
std::cout << "int: " << is_pointer_trait<int> << std::endl; // 输出: false

// 匹配偏特化 <T*>,其中 T 为 int
std::cout << "int*: " << is_pointer_trait<int*> << std::endl; // 输出: true

// 匹配偏特化 <T*>,其中 T 为 const char
std::cout << "const char*: " << is_pointer_trait<const char*> << std::endl; // 输出: true

// 匹配偏特化指向成员的指针
struct MyClass { int value; };
std::cout << "int MyClass::*: " << is_pointer_trait<int MyClass::*> << std::endl; // 输出: true

return 0;
}

在这个例子中,当我们使用 int* 实例化 is_pointer_trait 时,编译器发现 int* 符合偏特化 T* 的模式,因此选择了 true,而不是主模板的 false。偏特化的特征如下:

  • 模板参数列表不为空
  • 特化匹配(如 T*T&const T 等)
  • 适用于一类类型,而不是单个具体类型

2.2.2 数组类型检测

下面这段代码定义了一个编译时类型特征,用于检测一个类型是否是数组类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

// 主模板
template<typename T>
constexpr bool is_array_v = false;

// 偏特化1:已知大小的数组
template<typename T, std::size_t N>
constexpr bool is_array_v<T[N]> = true;

// 偏特化2:未知大小的数组
template<typename T>
constexpr bool is_array_v<T[]> = true;

// 偏特化3:多维数组
template<typename T, std::size_t N, std::size_t M>
constexpr bool is_array_v<T[N][M]> = true;

int main()
{
std::cout << std::boolalpha;
std::cout << "int: " << is_array_v<int> << std::endl; // false
std::cout << "int[5]: " << is_array_v<int[5]> << std::endl; // true
std::cout << "int[]: " << is_array_v<int[]> << std::endl; // true
std::cout << "int[3][4]: " << is_array_v<int[3][4]> << std::endl; // true

return 0;
}

上面程序的工作原理(模板匹配优先级)是这样的,比如当使用 is_array_v<int[5]> 时:

  • 编译器首先尝试匹配最特化的版本
  • T[N] 匹配成功:TintN5
  • 返回 true

2.3 匹配优先级

在使用变量模板的特化的时候可以同时定义了全特化偏特化,此时我们就需要知道它们的匹配优先级。编译器按以下顺序选择:

  1. 全特化(最具体)
  2. 偏特化(次具体)
  3. 主模板(最通用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <type_traits>

// 主模板 - 最低优先级
template<typename T>
constexpr const char* type_category = "unknown";

// 偏特化1:指针类型 - 中等优先级
template<typename T>
constexpr const char* type_category<T*> = "pointer";

// 偏特化2:引用类型 - 中等优先级
template<typename T>
constexpr const char* type_category<T&> = "lvalue reference";

// 全特化1:int 指针 - 最高优先级
template<>
constexpr const char* type_category<int*> = "int pointer";

// 全特化2:const char* - 最高优先级
template<>
constexpr const char* type_category<const char*> = "C-style string";

// 全特化3:void - 最高优先级
template<>
constexpr const char* type_category<void> = "void";

int main()
{
std::cout << "匹配测试:" << std::endl;
std::cout << "====================================" << std::endl;
std::cout << "int*: " << type_category<int*> << std::endl;
std::cout << "double*: " << type_category<double*> << std::endl;
std::cout << "int: " << type_category<int> << std::endl;
std::cout << "const char*: " << type_category<const char*> << std::endl;
std::cout << "void: " << type_category<void> << std::endl;

int x = 10;
int& ref = x;
std::cout << "int&: " << type_category<decltype(ref)> << std::endl;
std::cout << "int**: " << type_category<int**> << std::endl;

return 0;
}
  • type_category<int*>:匹配全特化,全特化优先于偏特化,因此匹配全特化 int pointer
  • type_category<double*>:匹配偏特化,没有 double*的全特化,匹配偏特化 pointer
  • type_category<int>:匹配主模板,没有匹配的特化,匹配主模板 unknown
  • type_category<const char*>:特殊字符串类型,匹配全特化 C-style string
  • type_category<void>:void 类型,匹配全特化 void
  • type_category<decltype(ref)>:引用类型,匹配偏特化 lvalue reference
  • type_category<int**>:多级指针,没有 int** 的全特化,匹配 T* 偏特化 ,结果是 pointer