C++ 非类型模板参数:将常量值作为模板参数的巧妙运用

C++ 非类型模板参数:让你的代码像乐高一样灵活

各位看官,咱们今天聊点C++里有点意思的东西——非类型模板参数。估计有些人一听“模板参数”就脑袋嗡嗡的,觉得高深莫测。别怕,其实这玩意儿说白了,就是让你可以把一些常量值,比如说数字、布尔值,甚至是字符,直接塞到模板里去,像搭乐高积木一样,拼出各种各样“定制化”的类型或函数。

是不是有点抽象?没事,咱先来个段子热热场。

话说,程序员小明最近接了个需求,要写个数组类,要求能指定数组的大小。普通的做法是,构造函数里传个size参数呗。但是,小明是个有追求的程序员,他觉得这样不够优雅!他想,数组的大小应该在编译期就确定下来,这样运行效率更高,而且类型系统也能帮他检查数组越界的问题。于是,他想到了非类型模板参数!

非类型模板参数,是啥玩意?

简单来说,就是模板参数不一定是类型,还可以是常量值。比如说,你可以这样写:

template <int N>
class MyArray {
private:
  int data[N]; // 数组大小在编译期就确定了!
public:
  MyArray() {
    std::cout << "MyArray of size " << N << " created!n";
  }
  int& operator[](size_t index) {
    if (index >= N) {
      throw std::out_of_range("Index out of bounds!");
    }
    return data[index];
  }
  const int& operator[](size_t index) const {
    if (index >= N) {
      throw std::out_of_range("Index out of bounds!");
    }
    return data[index];
  }
};

int main() {
  MyArray<10> arr10; // 创建一个大小为10的数组
  MyArray<20> arr20; // 创建一个大小为20的数组

  arr10[0] = 1;
  arr20[0] = 2;

  std::cout << arr10[0] << std::endl; // 输出 1
  std::cout << arr20[0] << std::endl; // 输出 2

  // arr10[10] = 3; // 编译时不会报错,运行时会抛出异常

  return 0;
}

看到没? template <int N> 里的 N 就是一个非类型模板参数,它代表一个整数常量。 MyArray<10>MyArray<20> 就分别创建了大小为 10 和 20 的数组。 关键是,这个大小是在编译期就确定了的,而不是在运行时才分配内存。

非类型模板参数的类型限制

当然,非类型模板参数也不是什么类型都能用的。 C++标准对它有一些限制,主要包括以下几种:

  • 整型类型: int, char, short, long, long long, bool 等,以及它们的 unsigned 版本。 这是最常用的类型。
  • 枚举类型: enum 定义的枚举类型。
  • 指针类型: 指向对象的指针或指向函数的指针,但必须是 nullptr 或者具有外部链接的对象的地址。 (后面会详细讲指针)
  • 引用类型: 指向对象的引用,但必须具有外部链接。
  • std::nullptr_t: nullptr 的类型。
  • 浮点数和类类型: C++20开始,允许使用浮点数和类类型作为非类型模板参数,但需要满足一些特定的约束(例如,constexpr构造函数等)。

为啥要用非类型模板参数?它的好处在哪?

  • 编译期确定大小: 就像上面的例子,数组的大小在编译期就确定了,避免了运行时的动态内存分配,提高了效率。
  • 类型安全: 编译器可以根据模板参数进行类型检查,例如,可以防止数组越界。
  • 代码复用: 可以通过改变模板参数,生成不同的类型或函数,而不需要重复编写代码。 想象一下,你写了一个矩阵类,可以用 template <int Rows, int Cols> 来指定矩阵的行列数,这样就可以轻松地创建各种大小的矩阵。
  • 更强的表达力: 可以把一些配置信息,比如最大重试次数、缓存大小等,直接放到模板参数里,让代码更清晰易懂。

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

除了上面数组的例子,非类型模板参数还有很多其他的用途。

  1. 矩阵类:
template <int Rows, int Cols>
class Matrix {
private:
  double data[Rows][Cols];
public:
  Matrix() {
    std::cout << "Matrix of size " << Rows << "x" << Cols << " created!n";
  }

  double& operator()(int row, int col) {
    if (row < 0 || row >= Rows || col < 0 || col >= Cols) {
      throw std::out_of_range("Index out of bounds!");
    }
    return data[row][col];
  }

  const double& operator()(int row, int col) const {
    if (row < 0 || row >= Rows || col < 0 || col >= Cols) {
      throw std::out_of_range("Index out of bounds!");
    }
    return data[row][col];
  }
};

int main() {
  Matrix<3, 4> matrix3x4; // 创建一个3x4的矩阵
  matrix3x4(0, 0) = 1.0;
  std::cout << matrix3x4(0, 0) << std::endl; // 输出 1.0
  return 0;
}
  1. 循环展开 (Loop Unrolling): 这是一种优化技术,可以减少循环的开销。
template <int N>
void unrolled_loop(int* arr) {
  for (int i = 0; i < N; ++i) {
    arr[i] = i * 2;
  }
}

int main() {
  int data[4];
  unrolled_loop<4>(data);

  for (int i = 0; i < 4; ++i) {
    std::cout << data[i] << " "; // 输出 0 2 4 6
  }
  std::cout << std::endl;
  return 0;
}

编译器可能会把这个循环展开,直接生成 arr[0] = 0; arr[1] = 2; arr[2] = 4; arr[3] = 6; 这样的代码,减少了循环的判断和跳转。

  1. 静态断言 (Static Assertions): 可以在编译期检查一些条件是否满足。
template <int Value>
struct CheckValue {
  static_assert(Value > 0, "Value must be positive!");
};

int main() {
  CheckValue<10> positiveValue; // OK
  // CheckValue<-5> negativeValue; // 编译错误:Value must be positive!
  return 0;
}

static_assert 会在编译期检查 Value > 0 是否成立,如果不成立,就会报错。

  1. 策略模式 (Policy-Based Design): 可以通过非类型模板参数选择不同的算法或策略。
enum class SortingAlgorithm {
  BubbleSort,
  QuickSort,
  MergeSort
};

template <SortingAlgorithm Algorithm>
void sort(int* arr, int size) {
  if constexpr (Algorithm == SortingAlgorithm::BubbleSort) {
    // 冒泡排序
    std::cout << "Using Bubble Sortn";
    // ... 冒泡排序的代码 ...
  } else if constexpr (Algorithm == SortingAlgorithm::QuickSort) {
    // 快速排序
    std::cout << "Using Quick Sortn";
    // ... 快速排序的代码 ...
  } else if constexpr (Algorithm == SortingAlgorithm::MergeSort) {
    // 归并排序
    std::cout << "Using Merge Sortn";
    // ... 归并排序的代码 ...
  }
}

int main() {
  int data[] = {5, 2, 8, 1, 9};
  int size = sizeof(data) / sizeof(data[0]);

  sort<SortingAlgorithm::QuickSort>(data, size); // 使用快速排序
  return 0;
}

这里,通过 SortingAlgorithm 这个枚举类型,我们可以选择不同的排序算法。 if constexpr 是 C++17 引入的特性,它允许在编译期进行条件判断,根据不同的条件生成不同的代码。

关于指针和引用作为非类型模板参数

前面提到,指针和引用也可以作为非类型模板参数,但这有一些限制。 它们必须指向具有外部链接的对象,或者是指向函数的指针。 这是因为编译器需要在编译期知道指针或引用的具体地址。

// 全局变量,具有外部链接
int global_value = 42;

template <int* P>
void print_value() {
  std::cout << "Value at address " << P << " is " << *P << std::endl;
}

int main() {
  print_value<&global_value>(); // OK
  return 0;
}

但是,下面的代码是错误的:

template <int* P>
void print_value() {
  std::cout << "Value at address " << P << " is " << *P << std::endl;
}

int main() {
  int local_value = 42;
  // print_value<&local_value>(); // 错误:local_value 没有外部链接
  return 0;
}

local_value 是一个局部变量,它没有外部链接,所以不能作为非类型模板参数。

一些需要注意的地方

  • 默认模板参数: 非类型模板参数也可以有默认值。
template <int N = 10>
class MyArray {
  // ...
};

int main() {
  MyArray<> arr; // N 默认为 10
  MyArray<20> arr20; // N 为 20
  return 0;
}
  • 模板参数推导: C++17 引入了类模板参数推导 (Class Template Argument Deduction, CTAD),可以根据构造函数的参数自动推导模板参数。 但是,对于非类型模板参数,通常需要显式指定。

  • constexpr: 非类型模板参数的值必须是编译期常量,所以通常要用 constexpr 来修饰。

总结

非类型模板参数是 C++ 模板编程中一个强大的工具,它可以让你在编译期控制类型的行为,提高代码的灵活性和效率。 虽然它有一些限制,但只要掌握了它的用法,就能写出更优雅、更高效的代码。

希望这篇文章能让你对非类型模板参数有一个更清晰的认识。 下次再遇到类似的需求,不妨试试用非类型模板参数来解决,说不定会有意想不到的收获! 记住,编程就像搭乐高,用好各种“积木”,就能创造出无限可能!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注