第3章 非类型模板参数

对于函数模板和类模板,模板参数不一定要是类型,也可以是常规的数值,和模板类型参数可以使类型细节留到被使用时再决定类似,非类型模板参数待定的不再是类型,而是数值。

使用时,需要显式地指出待定数值的具体值。

3.1 类模板的非类型参数

作为与之前的Stack对比,这里使用array类作为其容器。array可以避免内存管理,但是当管理的元素超过上限时就难以处理。但这里只专注于与vector的区别

#include <array>
#include <cassert>
template <typename T, std::size_t Maxsize>
class Stack{
private:
    std::array<T, Maxsize> elems;
    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 numElems == 0;}
	std::size_t size() const {return numElems;}
};

template<typename T, std::size_t Maxsize>
Stack<T,Maxsize>::Stack () : numElems(0){}

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
}

Maxsize是int类型的。通过它指定了Stack中array的大小

template<typename T, std::size_t Maxsize>
class Stack {
private:
	std::array<T,Maxsize> elems; // elements …
};

成员函数 push()也用它来检测 Stack 是否已满

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 中元素的类型和 Stack 的最大容量

#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();
}

上面每一次模板的使用都会实例化出一个新的类型。因此 int20Stack 和 int40Stack 是两种不同的类型,而且由于它们之间没有定义隐式或者显式的类型转换规则。也就不能使用其中一个取代另一个,或者将其中一个赋值给另一个。

对非类型模板参数,也可以指定默认值

template<typename T = int, std::size_t Maxsize = 100>
	class Stack { 
    // ...
};

但是从程序设计的角度来看,这可能不是一个好的设计方案。默认值应该是直观上正确的。不过对于一个普通的 Stack,无论是默认的 int 类型还是 Stack 的最大尺寸 100,看上去都不够直观。因此最好是让程序员同时显式地指定两个模板参数,这样在声明的时候这两个模板参数通常都会被文档化。

3.2 函数模板的非类型参数

类似的,可以给函数模板定义非类型模板参数

template<int Val, typename T>
T addValue (T x){
	return x + Val;
}

当该类函数或操作是被用作其他函数的参数时,可能会很有用。比如当使用C++标准库给一个集合中的所有元素增加某个值的时候,可以将这个函数模板的一个实例化版本用作第4个参数

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

第4个参数是从addValue<>()实例化出一个可以给传入的int型参数加5的函数实例。这个实例会被用来处理集合source中的所有元素,并将结果保存到目标集合dest中。

注意在这里必须将addValue<>()的模板参数T指定为int类型,因为类型推断只会对立即发生的调用起作用,而std::transfrom()又需要一个完整的类型来推断其第四个参数的类型。目前还不支持先部分地替换或推断模板参数的类型,然后再基于具体情况去推断其余的模板参数。

同样也可以基于前面的模板参数推断出当前模板参数的类型。比如可以通过传入的非类型模板参数推断出返回类型

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

或者可以通过如下方式确保传入的非类型模板参数的类型和类型参数的类型一致

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

3.3 非类型模板参数的限制

使用非类型模板参数是有限制的。通常它们只能是整形常量(包含枚举),指向objects/functions/members的指针,objects 或者 functions 的左值引用,或者是 std::nullptr_t(类型是 nullptr)。

浮点数数值或者class类型的对象都不能作为非类型模板参数使用。

当传递对象的指针或者引用作为模板参数时,对象不能是字符串常量,临时变量或者数据成员以及其他子对象。不过,C++版本的每次更新都会放宽以上限制

避免无效表达式

非类型模板参数可以是任何编译器表达式。比如:

template<int I, bool B>
class C; 
// ...
C<sizeof(int) + 4, sizeof(int)==4> c;

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

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开始,可以不指定非类型模板参数的具体类型(代之以auto),从而使其可以用于任意有效的非类型模板参数的类型。通过这一特性,可以定义如下更为泛化的大小固定的Stack类

#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 isempty
		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
}

通过使用 auto 的如下定义

template<typename T, auto Maxsize>
class Stack { 
    // ...
};

定义了类型待定的 Maxsize。它的类型可以是任意非类型参数所允许的类型

在模板内部,既可以使用它的值

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

也可以使用它的类型

using size_type = decltype(Maxsize);

然后可以将它用于成员函数 size()的返回类型:

size_type size() const { //return current number of elements
	return numElems;
}

从 C++14 开始,也可以通过使用 auto,让编译器推断出具体的返回类型

auto size() const { //return current number of elements
	return numElems;
}

根据这个类的声明,Stack 中 numElems 成员的类型是由非类型模板参数的类型决定的,当像下面这样使用它的时候

#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’;
	}
}

对于

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

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

对于

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

由于传递的非类型参数是 int,因此内部的 size_type 是 int 类型的

因为这两个 Stack 中成员函数 size()的返回类型是不一样的,所以

auto size1 = int20Stack.size(); 
auto size2 = stringStack.size();

中 size1 和 size2 的类型也不一样。这可以通过标准类型萃取 std::is_same和decltype 来验证

if (!std::is_same<decltype(size1), decltype(size2)>::value) {
	std::cout << "size types differ" << ’\n’;
}

输出结果将是:size types differ

从 C++17 开始,对于返回类型的类型萃取,可以通过使用下标_v 省略掉::value

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

注意关于非类型模板参数的限制仍然存在,比如它不能为double类型

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

#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 实例化成引用类型

template<decltype(auto) N>
class C {};
int i;
C<(i)> x; // i为int类型,但(i)为int&类型

3.4 总结

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

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐