C++C++17带有 auto 类型的非类型模板参数
苏丙榅1. 非类型模板参数
简单来说,非类型模板参数就是模板中可以传入的不是类型的参数。可以用生活中的例子来解释,想象一下现在有一个饼干模具:
- 类型模板参数:决定了模具的形状(圆形、方形、星形)
- 非类型模板参数:决定了模具的大小(直径5cm、直径10cm)
在代码中,非类型模板参数允许我们在编译时传入具体的值,而不是类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| template<typename T, int Size> class FixedArray { private: T data[Size]; public: int getSize() const { return Size; } };
FixedArray<int, 10> arr10; FixedArray<double, 100> arr100;
|
在 C++17 之前,当编写一个模板类或模板函数时,如果想让模板接收一个具体的值(比如一个数字、一个指针,而不是一个类型),必须显式地告诉 C++ 这个值的类型。
C++17 允许在非类型模板参数中使用 auto 关键字。这意味着编译器会自动推断传入的类型。我们不再需要关心传入的是 int, long, short 还是 size_t,只要它能用,编译器就会帮我们搞定。
基本语法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| template<auto Value> class MyClass { };
template<auto Value> void myFunction() { }
|
允许的类型:
非类型模板参数本身(即使加了 auto)也只能接受以下类型:
- 整形(
int, char, long, bool 等)
- 枚举类型
- 指针类型
- 左值引用类型
std::nullptr_t
- 浮点数类型(
float, double)(C++17不允许,C++20 放宽了限制,允许浮点数)。
示例代码:
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> #include <type_traits>
template<auto Value> struct AutoTemplate { static constexpr auto val = Value; void print() { std::cout << "Value: " << val << ", Type: " << typeid(val).name() << std::endl; } };
int main() { AutoTemplate<42> intInstance; AutoTemplate<'A'> charInstance; AutoTemplate<true> doubleInstance; intInstance.print(); charInstance.print(); doubleInstance.print(); return 0; }
|
2. 典型应用
2.1 模版特化
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 50 51 52 53 54 55
| #include <iostream>
template<auto Value> struct ValueProcessor { static void process() { std::cout << "Generic processing for value: " << Value << std::endl; } };
template<int Value> struct ValueProcessor<Value> { static void process() { std::cout << "Integral processing for value: " << Value << " (square: " << Value * Value << ")" << std::endl; } };
template<auto* Ptr> struct ValueProcessor<Ptr> { static void process() { std::cout << "Pointer processing for pointer: " << Ptr << std::endl; } };
template<> struct ValueProcessor<42> { static void process() { std::cout << "Special processing for the answer to everything!" << std::endl; } };
int main() { ValueProcessor<false>::process(); ValueProcessor<10>::process(); ValueProcessor<nullptr>::process(); ValueProcessor<42>::process(); static int x = 100; ValueProcessor<&x>::process(); return 0; }
|
因为非类型模板参数必须是编译期常量,所以template<auto* Ptr> 中的 Ptr,必须在编译代码的时候就已经确定它的值,而 &x(取地址)只有当 x 是静态存储期的变量时编译器才能确定。
1 2
| static int x = 100; ValueProcessor<&x>::process();
|
在非类型模板参数中使用指针时,C++ 标准(C++17 及 C++20)的核心要求是:该参数必须指向一个具有外部链接的对象。让我们看看 constexpr int x = 100; 定义在函数体内部时的链接属性:
1 2 3 4 5 6
| void main() { constexpr int x = 100; ValueProcessor<&x>::process(); }
|
利用 C++17 的非类型模板参数这个特性,我们可以创建非常灵活的序列生成器。
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
| #include <iostream>
template <auto... Args> struct Sequence {};
template <auto First, auto... Rest> struct Sequence<First, Rest...> { void print() { std::cout << First << " "; Sequence<Rest...> next; next.print(); } };
template <> struct Sequence<> { void print() { std::cout << "(End)" << std::endl; } };
int main() { Sequence<1, true, 'A', 42L> seq; std::cout << "Sequence values: "; seq.print();
return 0; }
|
当你实例化 Sequence<1, true, 'A', 42L> seq; 时,编译器实际上像剥洋葱一样在编译期生成了不同的类。让我们看看生成的调用链(注意:print() 是在运行时执行的,但对象的类型是编译期生成的):
第一次调用 (main函数中):
Sequence<1, true, 'A', 42L> 的 print() 被调用。
First 是 1。
动作:打印 1。
内部:创建 next 类型为 Sequence<true, 'A', 42L>。
第二次调用 (递归):
Sequence<true, 'A', 42L> 的 print() 被调用。
First 是 true。
动作:打印 1 (因为 bool 的 cout 默认输出是 1)。
内部:创建 next 类型为 Sequence<'A', 42L>。
第三次调用 (递归):
第四次调用 (递归):
第五次调用 (终止):
程序运行后的输出会是:
1
| Sequence values: 1 1 A 42 (End)
|
true 被输出为 1,这是 std::cout 对 bool 类型的标准行为(除非使用 std::boolalpha)。
这段代码的递归是在运行时 通过创建对象 next 和调用函数 next.print() 进行的。在 C++11/14 乃至优化较差的编译器下,这会导致函数调用栈的开销。如果列表很长(比如几千个元素),可能会导致栈溢出。为了消除运行时的函数递归开销,可以使用 constexpr 函数配合折叠表达式,将递归逻辑扁平化:
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> #include <type_traits>
template <auto... Args> struct Sequence { void print() { std::cout << "Sequence values: "; ((std::cout << Args << " "), ...); std::cout << "(End)" << std::endl; } void print_with_types() { std::cout << "Sequence with types: "; ((std::cout << "(" << Args << ":" << typeid(Args).name() << ") "), ...); std::cout << std::endl; } };
int main() { Sequence<1, true, 'A', 42L> seq; seq.print();
return 0; }
|
折叠表达式说明:
((std::cout << Args << " "), ...) :一元右折叠,使用逗号运算符 , 作为操作符。
, :连接多个表达式,确保从左到右执行顺序,返回最后一个表达式的值
- 这是 C++17 引入的特性,可以替代复杂的递归模板展开
折叠表达式详解