C++ Template-3 非类型模板参数

通过大量例子介绍了非类型模板参数。你可以学习到如何使用非类型模板参数,了解非类型模板参数使用的限制。

不是本系列第一篇文章,推荐阅读完前面的文章再看本文,以下是本系列文章目录。

1. 函数模板

2. 类模板

3. 非类型模板参数(本文)

4. 可变参数模板

5. 基础技术

6. 移动语意与 enable_if<>

3. 非类型模板参数

对于函数模板和类模板,模板参数不一定必须为类型,它们也可以是普通的值。和使用类型作为模板参数类似,你可以使代码的部分细节留到使用的时候再确定。只是,这些待定的细节不再是类型,而是某个数值。当我们使用这种模板的时候,你必须显式地表明出数值的具体值,之后代码才会被实例化。本章会通过一个新版本的 stack 类模板来说明这一新特性。顺便我们会介绍一下函数模板的非类型参数的使用,并讨论这一技术的一些局限性。

3.1 类模板的非类型参数

作为和前面章节实现的 stack 的对比,你可以实现一个用一个固定大小的数组作为容器的 stack。这种方式的优点是可以避免开发者或者标准库容器管理内存的开销。不同,确定一个合适的大小是一个难题。如果指定的值太小,那么 stack 容器就容易满。如果指定的大小太大,可能造成内存的浪费。因此最好让用户根据自身的情况合理确定 stack 的大小

为此,可以将 stack 的大小定义成模板参数:

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
//stacknontype.hpp
#include <array>
#include <cassert>

template <typename T, std::size_t Maxsize>
class Stack
{
private:
std::array<T, Maxsize> elems; // elements
std::size_t numElems; // current number of elements
public:
Stack(); // constructor
void push(T const& elem); // push element
void pop(); // pop element
T const& top() const; // return top element
bool empty() const
{ // return whether the stack is empty
return numElems == 0;
}
std::size_t size() const
{ // return current number of elements
return numElems;
}
};

template <typename T, std::size_t Maxsize>
Stack<T, Maxsize>::Stack()
: numElems(0) // start with no elements
{
// nothing else to do
}

template <typename T, std::size_t Maxsize>
void Stack<T, Maxsize>::push(T const& elem)
{
assert(numElems < Maxsize);
elems[numElems] = elem; // append element
++numElems; // increment number of elements
}

template <typename T, std::size_t Maxsize>
void Stack<T, Maxsize>::pop()
{
assert(!elems.empty());
--numElems; // decrement number of elements
}

template <typename T, std::size_t Maxsize>
T const &Stack<T, Maxsize>::top() const
{
assert(!elems.empty());
return elems[numElems - 1]; // return last element
}

第二个新的模板参数 Maxsizeint 类型的,它指定了 stack 中数组中的大小:

1
2
3
4
5
6
7
template <typename T, std::size_t Maxsize>
class Stack
{
private:
std::array<T, Maxsize> elems; // elements
...
};

另外,成员函数 push() 在使用时也检测了 stack 是否已满:

1
2
3
4
5
6
7
template <typename T, std::size_t Maxsize>
void Stack<T, Maxsize>::push(T const& elem)
{
assert(numElems < Maxsize);
elems[numElems] = elem; // append element
++numElems; // increment number of elements
}

为了使用这个类模板你必须同时显式地写出 stack 中元素的类型和其最大容量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "stacknontype.hpp"
#include <iostream>
#include <string>

int main()
{
Stack<int,20> int20Stack; // stack of up to 20 ints
Stack<int,40> int40Stack; // stack of up to 40 ints
Stack<std::string,40> stringStack; // stack of up to 40 strings

// manipulate stack of up to 20 ints
int20Stack.push(7);
std::cout << int20Stack.top() << '\n';
int20Stack.pop();

// manipulate stack of up to 40 strings
stringStack.push("hello");
std::cout << stringStack.top() << '\n';
stringStack.pop();
}

注意,上面每一个模板都实例化出一个不同的类型。因此,int20Stackint40Stack 它们的类型是不同的,而且它们之间没有定义隐式或者是显式的类型转换规则。因此,不能用其中一个取代另一个,也不能将其中一个赋值给另外一个。

同时,对于非类型模板参数来说,也可以指定默认值:

1
2
3
4
5
template <typename T = int, std::size_t Maxsize = 100>
class Stack
{
...
};

但是,从良好的程序设计的角度来看,这可能不是一个合适的例子。默认值应该直观地看上去是正确的。不过对于默认类型是 int 或者是默认大小是 100 对于一般的 stack 模板看上去都不够直观。因此,最好是让程序员同时显式地指定这两个模板参数,这样在声明的时候这个两个模板参数都可以被文档化(documented)

3.2 非类型函数模板参数

你也可以为函数模板定义非类型模板参数。例如,下面这个函数模板定义了一组可以添加一个确定的值的函数:

1
2
3
4
5
template <int Val, typename T>
T addValue(T x)
{
return x + Val;
}

这类函数或者操作作用于其他函数的参数时,会十分有用。例如,当你使用 C++ 标准库时,你可以传递实例化后的次函数模板来为每一个元素添加一个值:

1
2
3
std::transform(source.begin(), source.end(), // start and end of source
dest.begin(), // start of destination
addValue<5,int>); // operation

最后一个参数是 addValue<>() 实例化出可以传入 int 类型的值,并为其加 5 的函数实例。这个函数会用来处理 source 中的每一个元素,必将结果保存在 dest 中。

注意在这一你必须显式指出 addValue<>() 的模板参数 T 的类型是 int。因为模板参数类型推导只会在立即发生的调用中起作用,但是 std::transform() 需要一个完整的类型去推导第四个模板参数。目前还不支持部分替换或推导模板参数然后再根据情况推导出剩余的模板参数。

同样,你也可以指出模板参数是从前一个参数推导出来的。比如,通过传入的非类型模板参数来推导出返回类型:

1
2
template <auto Val, typename T = decltype(Val)>
T foo();

或者可以通过以下方式确保非类型模板参数的类型和返回类型一样:

1
2
template <typename T, T Val = T{}>
T bar();

3.3 非类型模板参数的限制

注意非类型模板参数会带来某些限制。通常,他们只可以是整型的常量(包括枚举),指向 objects/functions/members 的指针,objects/functions 的左值引用,或者 std::nullptr_tnullptr 的类型)。

浮点型数值或者 class 类型的对象是不允许作为非类型模板参数的:

1
2
3
4
5
6
7
8
9
10
template <double VAT>      // ERROR: floating-point values are not
double process (double v) // allowed as template parameters
{
return v * VAT;
}
template <std::string name> // ERROR: class-type objects are not
class MyClass // allowed as template parameters
{
...
};

当传递指针或者引用给模板参数时,这个对象不能为字符串常量,临时变量或者数据成员以及其他子对象。在 C++17 之前,每个 C++ 版本的更新都会放宽这个限制:

  • 在 C++11 中,对象必须要有外部链接。
  • 在 C++14 中,对象必须要有外部链接或者内部链接。

因此下面的写法是不正确的:

1
2
3
4
5
6
7
template <char const* name>
class MyClass
{
...
};
MyClass<"hello"> x; // ERROR: class-type objects are not
// allowed as template parameters

但是有如下的变通方法(视 C++ 版本而定):

1
2
3
4
5
6
7
8
9
10
extern char const s03[] = "hi";  // external linkage
char const s11[] = "hi"; // internal linkage

int main()
{
Message<s03> m03; // OK(all versions)
Message<s11> m11; // OK since C++11
static char const s17[] = "hi"; // no linkage
Message<s17> m17; // OK since C++17
}

在上面三种情况中字符数组都是被 "hi" 初始化,这个对象被用来作为被声明为 char const* 的模板参数。如果这个对象有外部链接(s03),那么在所有版本都是有效的,如果对象有内部链接,那么对 C++11 和 C++14 也是有效的,从 C++17开始,即使对象没有链接属性也是有效的。

12.3.3 节(未写)对这一问题进行了更加详细的讨论,17.2 节(未写)对这一问题未来的变化进行了讨论。

避免无效的表达式

非类型模板参数可以使任何编译期的表达式,比如:

1
2
3
4
template <int T, bool B>
class C;
...
C<sizeof(int) + 4, sizeof(int) == 4> c;

不过如果在表达式中使用了 operator >,就必须将相应的表达式放在括号里面,否则 > 会被认为模板参数列表末尾的 >:

1
2
C<42, sizeof(int) > 4> c; // ERROR: first > ends the template argument list
C<42, (sizeof(int) > 4)> c; // OK

3.4 模板参数类型 auto

从 C++17 开始,你可以定义非类型模板参数来接受任何允许的非类型模板参数。通过这一特性,我们可以设计一个更加泛化的有固定大小的 stack:

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
56
57
58
//stackauto.hpp
#include <array>
#include <cassert>

template <typename T, auto Maxsize>
class Stack
{
public:
using size_type = decltype(Maxsize);

private:
std::array<T, Maxsize> elems; // elements
size_type numElems; // current number of elements
public:
Stack(); // constructor
void push(T const& elem); // push element
void pop(); // pop element
T const &top() const; // return top element
bool empty() const // return whether the stack is empty
{
return numElems == 0;
}
size_type size() const // return current number of elements
{
return numElems;
}
};

// constructor
template <typename T, auto Maxsize>
Stack<T, Maxsize>::Stack()
: numElems(0) // start with no elements
{
// nothing else to do
}

template <typename T, auto Maxsize>
void Stack<T, Maxsize>::push(T const& elem)
{
assert(numElems < Maxsize);
elems[numElems] = elem; // append element
++numElems; // increment number of elements
}

template <typename T, auto Maxsize>
void Stack<T, Maxsize>::pop()
{
assert(!elems.empty());
--numElems; // decrement number of elements
}

template <typename T, auto Maxsize>
T const &Stack<T, Maxsize>::top() const
{
assert(!elems.empty());
return elems[numElems - 1]; // return last element
}

通过定义

1
2
3
4
5
template <typename T, auto Maxsize>
class Stack
{
...
};

通过使用 placeholder type auto,你定义了类型待定的 Maxsize。它的类型可以使任何费类型模板参数所允许的类型。

在模板内存,你可以直接使用它的值:

1
std::array<T, Maxsize> elems; // elements

也可以使用它的类型:

1
using size_type = decltype(Maxsize);

然后也可以作为 size() 函数的返回类型:

1
2
3
4
size_type size() const    // return current number of elements
{
return numElems;
}

从 C++ 14 开始,你也可以只使用 auto 作为返回值,让编译器推导出返回的类型:

1
2
3
4
auto size() const        // return current number of elements
{
return numElems;
}

根据类的声明,numElems 的类型是由非类型模板参数的类型来决定的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <string>
#include "stackauto.hpp"

int main()
{
Stack<int,20u> int20Stack; // stack of up to 20 ints
Stack<std::string,40> stringStack; // stack of up to 40 strings

// manipulate stack of up to 20 ints
int20Stack.push(7);
std::cout << int20Stack.top() << '\n';
auto size1 = int20Stack.size();

// manipulate stack of up to 40 strings
stringStack.push("hello");
std::cout << stringStack.top() << '\n';
auto size2 = stringStack.size();

if (!std::is_same_v<decltype(size1), decltype(size2)>)
std::cout << "size types differ" << '\n';
}

对于

1
Stack<int,20u> int20Stack;     // stack of up to 20 ints

由于传递的非类型模板参数是 20u,因此 size_type 的类型是 unsigned int

对于

1
Stack<std::string,40> stringStack; // stack of up to 40 strings

因为传递的非类型模板参数是 40, 因此 size_type 的类型是 int

两种 stack 的 size() 函数的返回类型是不同的,所以

1
2
3
auto size1 = int20Stack.size();
...
auto size2 = stringStack.size();

中的 size1size2 的类型也是不同的。通过使用 standard type trait std::is_samedecltype,可以验证:

1
2
if (!std::is_same<decltype(size1), decltype(size2)>::value) 
std::cout << "size types differ" << '\n';

输出的结果是:

1
size types differ

从 C++17 开始,对于返回类型的 type trait,可以通过后缀 _v 来省略 ::value参考5.6节 (未写)):

1
2
if (!std::is_same_v<decltype(size1), decltype(size2)>) 
std::cout << "size types differ" << '\n';

注意非类型模板的限制依然存在。尤其是那些 3.3 节讨论的限制。比如:

1
Stack<int, 3.14> sd; // ERROR: Floating-point nontype argument

由于你可以传递字符串常量数组作为非类型模板参数(从 C++17 开始甚至可以是静态的局部变量,参见 3.3 节),下面的用法也是正确的:

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

template<auto T> // take value of any possible nontype parameter (since C++17)
class Message
{
public:
void print()
{
std::cout << T << '\n';
}
};

int main()
{
Message<42> msg1;
msg1.print(); // initialize with int 42 and print that value

static char const s[] = "hello";
Message<s> msg2; // initialize with char~const[6] "hello"
msg2.print(); // and print that value
}

也可以使用 template <decltype(auto) N>,这样可以将 N 实例化为引用类型:

1
2
3
4
5
6
7
template <decltype(auto) N>
class C
{
...
};
int i;
C<(i)> x; // N is int&

更多细节参见 15.10.1 节(未写)

3.5 小结

  • 模板参数不只可以是类型,也可以是值。
  • 你不可以将浮点类型和 class 类型对象作为非类型模板参数。使用字符串常量,临时变量,子对象的指针和引用也会有一些限制。
  • 通过使用关键字 auto,可以使非类型模板参数的类型更加泛化。

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!