跳到主要内容

C++ 非类型模板参数

介绍

在C++模板编程中,我们通常会使用类型作为模板参数,例如 std::vector<int> 中的 int。但C++还支持另一种强大的模板参数形式:非类型模板参数(Non-type Template Parameters,简称NTTP)。这种参数不是类型,而是在编译时就能确定的常量值,如整数、枚举值、指针或引用等。

非类型模板参数使得我们能够根据常量值来定制模板的行为,增强了模板的灵活性和实用性。

提示

非类型模板参数在编译时就被确定,这意味着使用它们可以实现编译期计算和优化,避免运行时的开销。

基本语法

非类型模板参数的基本语法如下:

cpp
template <typename T, 数据类型 名称>
class/struct/函数 {
// 实现
};

其中,"数据类型"可以是整数类型、枚举类型、指针、引用等。

常见的非类型模板参数类型

非类型模板参数可以使用的类型有限制,C++17之前主要支持以下几种:

  1. 整型常量(int, long, char等)
  2. 枚举类型
  3. 指向对象/函数的指针
  4. 对象/函数的引用
  5. 指向成员的指针

C++20进一步放宽了限制,允许使用浮点类型和字面量类等。

基本示例

示例1:整数作为模板参数

cpp
#include <iostream>

template <typename T, int Size>
class Array {
private:
T data[Size];
public:
// 获取数组大小
constexpr int size() const { return Size; }

// 访问元素
T& operator[](int index) {
return data[index];
}

// 常量版本的访问操作符
const T& operator[](int index) const {
return data[index];
}
};

int main() {
Array<int, 5> arr;

for (int i = 0; i < arr.size(); ++i) {
arr[i] = i * 10;
}

for (int i = 0; i < arr.size(); ++i) {
std::cout << "arr[" << i << "] = " << arr[i] << std::endl;
}

return 0;
}

输出结果:

arr[0] = 0
arr[1] = 10
arr[2] = 20
arr[3] = 30
arr[4] = 40

在这个例子中,我们定义了一个模板类 Array,它接受两个模板参数:元素类型 T 和数组大小 Size。数组大小 Size 是一个非类型模板参数,在实例化模板时必须提供一个具体的常量值。

示例2:使用多个非类型模板参数

cpp
#include <iostream>

template <typename T, int Rows, int Cols>
class Matrix {
private:
T data[Rows][Cols];

public:
// 构造函数,初始化所有元素为0
Matrix() {
for (int i = 0; i < Rows; ++i) {
for (int j = 0; j < Cols; ++j) {
data[i][j] = T();
}
}
}

// 设置元素值
void set(int row, int col, const T& value) {
if (row >= 0 && row < Rows && col >= 0 && col < Cols) {
data[row][col] = value;
}
}

// 获取元素值
T get(int row, int col) const {
if (row >= 0 && row < Rows && col >= 0 && col < Cols) {
return data[row][col];
}
return T();
}

// 打印矩阵
void print() const {
for (int i = 0; i < Rows; ++i) {
for (int j = 0; j < Cols; ++j) {
std::cout << data[i][j] << " ";
}
std::cout << std::endl;
}
}
};

int main() {
Matrix<int, 2, 3> matrix;

matrix.set(0, 0, 1);
matrix.set(0, 1, 2);
matrix.set(0, 2, 3);
matrix.set(1, 0, 4);
matrix.set(1, 1, 5);
matrix.set(1, 2, 6);

std::cout << "2x3 Matrix:" << std::endl;
matrix.print();

return 0;
}

输出结果:

2x3 Matrix:
1 2 3
4 5 6

在这个例子中,我们创建了一个 Matrix 类模板,使用三个模板参数:元素类型 T 以及两个非类型模板参数 RowsCols,分别表示矩阵的行数和列数。

非类型模板参数的限制

虽然非类型模板参数非常有用,但它们也有一些限制:

  1. 必须是编译时常量
  2. 在C++20之前,不能使用浮点数类型
  3. 不能使用字符串字面量(但可以使用字符数组,如C++17的std::array<char, N>
  4. 模板参数不能依赖于另一个模板参数的值(C++17之前)
警告

非类型模板参数的值必须在编译时可以确定,不能是运行时的变量。

使用常量表达式作为非类型模板参数

非类型模板参数不仅可以是简单的常量,还可以是常量表达式:

cpp
#include <iostream>

template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N-1>::value;
};

template <>
struct Factorial<0> {
static constexpr int value = 1;
};

int main() {
std::cout << "Factorial of 5: " << Factorial<5>::value << std::endl;
std::cout << "Factorial of 10: " << Factorial<10>::value << std::endl;

return 0;
}

输出结果:

Factorial of 5: 120
Factorial of 10: 3628800

这个例子展示了如何使用模板元编程和非类型模板参数在编译时计算阶乘。Factorial<N>::value 在编译时就被计算出来,而不是在运行时。

C++ 17 和 C++20 中的改进

C++ 17:auto 非类型模板参数

C++17允许使用 auto 来声明非类型模板参数,使模板更加灵活:

cpp
template <auto Value>
class Container {
public:
decltype(Value) get() const { return Value; }
};

int main() {
Container<42> c1; // Value 的类型是 int
Container<'a'> c2; // Value 的类型是 char
Container<3.14> c3; // 在C++20中才有效

std::cout << c1.get() << std::endl;
std::cout << c2.get() << std::endl;

return 0;
}

输出结果:

42
a

C++ 20:class类型的非类型模板参数

C++20进一步扩展了非类型模板参数的能力,允许使用具有特定约束的类类型作为非类型模板参数:

cpp
#include <iostream>
#include <string_view>

struct Point {
int x;
int y;

// 必须有operator==
constexpr bool operator==(const Point&) const = default;
};

template <Point P>
void printPoint() {
std::cout << "Point: (" << P.x << ", " << P.y << ")" << std::endl;
}

int main() {
constexpr Point p1{1, 2};
constexpr Point p2{3, 4};

printPoint<p1>();
printPoint<p2>();

return 0;
}

输出结果:

Point: (1, 2)
Point: (3, 4)

要使用类类型作为非类型模板参数,该类型必须满足以下条件:

  1. 所有非静态数据成员都是public的
  2. 类不含有虚函数或虚基类
  3. 类必须有一个可用的比较运算符(operator==
  4. 所有非静态数据成员和基类也必须满足这些要求

实际应用案例

案例1:编译时固定大小的缓冲区

cpp
#include <iostream>
#include <algorithm>

template <typename T, std::size_t N>
class CircularBuffer {
private:
T data[N];
std::size_t head = 0;
std::size_t tail = 0;
bool full = false;

public:
bool push(const T& item) {
if (full) {
return false;
}

data[tail] = item;
tail = (tail + 1) % N;
full = (head == tail);

return true;
}

bool pop(T& item) {
if (empty()) {
return false;
}

item = data[head];
head = (head + 1) % N;
full = false;

return true;
}

bool empty() const {
return (!full && (head == tail));
}

bool isFull() const {
return full;
}

std::size_t size() const {
if (full) return N;
if (tail >= head) return tail - head;
return N - (head - tail);
}

std::size_t capacity() const {
return N;
}
};

int main() {
CircularBuffer<int, 5> buffer;

std::cout << "Pushing elements: ";
for (int i = 0; i < 7; ++i) {
if (buffer.push(i)) {
std::cout << i << " ";
} else {
std::cout << "(buffer full, couldn't add " << i << ") ";
}
}
std::cout << std::endl;

std::cout << "Buffer size: " << buffer.size() << std::endl;
std::cout << "Buffer capacity: " << buffer.capacity() << std::endl;

std::cout << "Popping elements: ";
int value;
while (buffer.pop(value)) {
std::cout << value << " ";
}
std::cout << std::endl;

return 0;
}

输出结果:

Pushing elements: 0 1 2 3 4 (buffer full, couldn't add 5) (buffer full, couldn't add 6) 
Buffer size: 5
Buffer capacity: 5
Popping elements: 0 1 2 3 4

这个例子实现了一个环形缓冲区,其容量在编译时就通过非类型模板参数 N 确定。这样可以避免动态内存分配,提高效率,特别适合嵌入式系统或实时应用。

案例2:编译时数组操作

cpp
#include <iostream>
#include <array>
#include <algorithm>
#include <numeric>

template <typename T, std::size_t N>
class StaticArray {
private:
std::array<T, N> data;

public:
// 使用初始值列表构造
StaticArray(const std::initializer_list<T>& values) {
std::size_t i = 0;
for (const auto& value : values) {
if (i >= N) break;
data[i++] = value;
}
// 如果初始值列表长度小于N,用默认值填充剩余部分
while (i < N) {
data[i++] = T();
}
}

// 求和
T sum() const {
return std::accumulate(data.begin(), data.end(), T());
}

// 求平均值
double average() const {
return static_cast<double>(sum()) / N;
}

// 查找最大值
T max() const {
return *std::max_element(data.begin(), data.end());
}

// 查找最小值
T min() const {
return *std::min_element(data.begin(), data.end());
}

// 排序(修改原数组)
void sort() {
std::sort(data.begin(), data.end());
}

// 打印数组
void print() const {
for (const auto& item : data) {
std::cout << item << " ";
}
std::cout << std::endl;
}
};

int main() {
StaticArray<int, 6> arr = {5, 2, 9, 1, 7, 3};

std::cout << "原始数组: ";
arr.print();

std::cout << "数组总和: " << arr.sum() << std::endl;
std::cout << "数组平均值: " << arr.average() << std::endl;
std::cout << "最大值: " << arr.max() << std::endl;
std::cout << "最小值: " << arr.min() << std::endl;

arr.sort();
std::cout << "排序后: ";
arr.print();

return 0;
}

输出结果:

原始数组: 5 2 9 1 7 3 
数组总和: 27
数组平均值: 4.5
最大值: 9
最小值: 1
排序后: 1 2 3 5 7 9

这个例子展示了如何使用非类型模板参数创建固定大小的数组类,并实现常见的数组操作。由于数组大小在编译时已知,编译器可以进行更多优化。

总结

非类型模板参数是C++模板编程中非常强大的功能,它允许我们根据编译时已知的常量值来定制模板的行为。主要优点包括:

  1. 编译时优化:非类型模板参数在编译时确定,允许编译器进行更多优化
  2. 类型安全:与使用运行时参数相比,编译时检查可以提前发现错误
  3. 性能提升:避免了运行时的计算开销
  4. 代码复用:可以基于不同的常量值生成专门的代码

随着C++标准的发展,非类型模板参数的能力不断扩展。C++17引入了auto非类型模板参数,C++20则允许使用更多类型作为非类型模板参数,包括浮点数和满足特定条件的类类型。

练习

  1. 创建一个 Power 模板,使用非类型模板参数计算编译时的幂运算。例如 Power<2, 8>::value 应该是 256。

  2. 实现一个固定大小的栈模板,使用非类型模板参数指定最大容量。

  3. 创建一个对于N×N矩阵进行操作的模板类,支持矩阵加法、乘法等基本操作。

  4. 尝试使用C++20的特性,创建一个以自定义类型为非类型模板参数的模板。

附加资源

备注

为了更好地掌握非类型模板参数,建议你先巩固C++模板的基础知识,然后通过实践不断加深对这一特性的理解。