本系列是对 C++ Templates The Complete Guide Second Edition 的学习和解读。本文介绍类模板。
和函数模板类似,类也可以实现部分类型的参数化。C++ 标准库的容器类就是类模板很好的例子。
类模板的实现
本文以数据结构中的栈(Stack
)为例介绍类模板。
#include <vector>
#include <cassert>
template<typename T>
class Stack {
private:
std::vector<T> elems;
public:
void push(T const& elem);
void pop();
T const& top() const;
bool empty() const {
return elems.empty();
}
};
template<typename T>
void Stack<T>::push (T const& elem)
{
elems.push_back(elem);
}
template<typename T>
void Stack<T>::pop ()
{
assert(!elems.empty());
elems.pop_back();
}
template<typename T>
T const& Stack<T>::top () const
{
assert(!elems.empty());
return elems.back();
}
本例中,Stack
的类模板实现借助了标准库的 vector<>
,节省了内存管理、构造函数、析构函数等的实现,可以聚焦于类模板的语法。
类模板的申明和函数模板的申明类似。
template<typename T>
class Stack {
...
};
也可以使用关键字 class
代替 typename
,但是为了避免混淆,建议优先使用 typename
。
template<class T>
class Stack {
...
};
在模板类内部,模板参数 T
可以像普通类型那样用于申明成员变量和成员函数。Stack
类的类型是 Stack<T>
,因此,除了模板参数可以被推导的场景外,任何使用该类的类型的地方都必须使用 Stack<T>
。但是,在类模板内部,可以省去类名后的模板参数。例如,你需要申明自己的拷贝构造函数和赋值操作:
emplate<typename T>
class Stack {
...
Stack (Stack const&); // copy constructor
Stack& operator= (Stack const&); // assignment operator
...
};
但是,在类模板外,则必须用完整的类型 Stack<T>
:
template<typename T>
bool operator== (Stack<T> const& lhs, Stack<T> const& rhs);
在类外定义成员函数时,必须要使用 template<Typename T>
指明这是一个模板。例如:
template<typename T>
void Stack<T>::push (T const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
类模板的使用
在 C++17 之前,使用类模板对象的时候,必须显示指定模板参数。
int main()
{
Stack<int> intStack; // stack of ints
Stack<std::string> stringStack; // stack of strings
// manipulate int stack
intStack.push(7);
std::cout << intStack.top() << ’\n’;
// manipulate string stack
stringStack.push("hello");
std::cout << stringStack.top() << ’\n’;
stringStack.pop();
}
类的模板参数可以是任何类型。例如:
Stack<float*> floatPtrStack; // stack of float pointers
Stack<Stack<int>> intStackStack; // stack of stack of ints
但是,需要注意的是: 在 C++11 之前,两个右尖括号之间必须留有空白:
Stack<Stack<int> > intStackStack; // OK with all C++ versions
Stack<Stack<int>> intStackStack; // ERROR before C++11
类模板的成员函数只有在被调用的时候才实例化。 这可以节省时间和空间,也使得允许模板类的部分使用(partial usage)。
类模板的部分使用
前面也提到过,类模板的成员变量只在需要的时候(被调用)才会实例化,也即支持类模板的部分使用。例如:
template<typename T>
class Stack {
...
void printOn() (std::ostream& strm) const {
for (T const& elem : elems) {
strm << elem << ’ ’; // call << for each element
}
}
};
Stack<std::pair<int,int>> ps; // note: std::pair<> has no operator<< defined
ps.push({4, 5}); // OK
ps.push({6, 7}); // OK
std::cout << ps.top().first << ’\n’; // OK
std::cout << ps.top().second << ’\n’; // OK
ps.printOn(std::cout); // ERROR: operator<< not supported for element type
因为类模板成员函数只有在调用的时候才实例化,因而 ps.printOn(std::cout);
语句会产生编译报错。
友元
一般使用重载 <<
的方式实现类的打印功能。可以通过友元的方式重载 operator<<
。
template<typename T>
class Stack {
...
void printOn(std::ostream& os) const;
friend std::ostream& operator<<(std::ostream& os, const Stack<T>& stack)
{
stack.printOn(os);
return os;
}
};
如果在类内声明,类外定义,情况稍有复杂。如下实现,则编译报错:
template<typename T>
class Stack {
...
friend std::ostream& operator<<(std::ostream&, const Stack<T>);
};
std::ostream& operator<<(std::ostream& os,
const Stack<T>& stack)
{
stack.printOn(os);
return os;
}
因为类外定义时, T
不可见。一般有两种解决方案。
- 声明一个新的模板函数,使用不同的模板参数。
template<typename T>
class Stack {
…
template<typename U>
friend std::ostream& operator<<(std::ostream&, const Stack<U>&);
};
template<typename U>
std::ostream& operator<<(std::ostream& os, const Stack<U>& stack)
{
stack.printOn(os);
return os;
}
- 前置申明类模板和友元函数模板。
template<typename T> // operator<<中参数中要求Stack模板可见
class Stack;
template<typename T>
std::ostream& operator<<(std::ostream&, const Stack<T>&);
// 随后就可以将其声明为友元
template<typename T>
class Stack {
…
// 加上<>是强制为模板函数,不加就是普通函数
friend std::ostream& operator<< <T> (std::ostream&, const Stack<T>&);
// friend std::ostream& operator<< <T> (std::ostream&, const Stack<T>&); is ok!
};
template<typename T>
std::ostream& operator<<(std::ostream& os, const Stack<T>& stack)
{
stack.printOn(os);
return os;
}
这种方式,需要在函数名 operator<<
后加 <T>
或 <>
表明是函数模板。
int main() {
Stack<int> is;
is.push(3);
is.push(4);
std::cout << is << std::endl;
Stack<std::pair<int,int>> ps;
ps.push({4, 5});
ps.push({6, 7});
std::cout << ps << std::endl; // ERROR
return 0;
}
同样地,友元函数模板只有被调用时才实例化,元素没有定义 operator<<
时也可以使用这个类,只有调用 operator<<
时才会出错。
类模板特例化
类似于函数模板的重载,类模板可以使用特定的类型来特例化。类模板的特例化可以用于优化特定类型的实现,也可以用于修复特定类型实例化的错误行为。
一旦实例化了一个类模板,你需要实例化它所有的成员函数。虽然可以实例化类模板的一个成员函数,一旦你这样做了,你将不能再实例化这个类模板了。
实例化一个类模板,需要 template<>
打头。类外定义成员函数就可以像 ”普通“ 函数那样了。
#include <deque>
#include <string>
#include <cassert>
template<>
class Stack<std::string> {
private:
std::deque<std::string> elems;
public:
void push(std::string const&);
void pop();
std::string const& top() const;
bool empty() const {
return elems.empty();
}
};
void Stack<std::string>::push (std::string const& elem)
{
elems.push_back(elem);
}
void Stack<std::string>::pop ()
{
assert(!elems.empty());
elems.pop_back();
}
std::string const& Stack<std::string>::top () const
{
assert(!elems.empty());
return elems.back();
}
类模板偏特化
类模板可以部分特例化,也即偏特化。偏特化介于特例化完全模板之间,可以针对某些类型特殊实现,又可以保留一部分模板参数交给用户定义。例如针对指针类型的 Stack
实现:
// partial specialization of class Stack<> for pointers:
template<typename T>
class Stack<T*> {
private:
std::vector<T*> elems;
public:
void push(T*);
T* pop();
T* top() const;
bool empty() const {
return elems.empty();
}
};
template<typename T>
void Stack<T*>::push (T* elem)
{
elems.push_back(elem);
}
template<typename T>
T* Stack<T*>::pop ()
{
assert(!elems.empty());
T* p = elems.back();
elems.pop_back();
return p;
}
template<typename T>
T* Stack<T*>::top () const
{
assert(!elems.empty());
return elems.back();
}
该例为指针类型的 Stack
偏特化。偏特化可以提供不同的接口。例如 pop()
返回了指针,当指针用 new
创建时,用户可以使用 delete
清除。
模板偏特化也可能产生二义性。例如:
template<typename T1, typename T2>
class MyClass {
...
};
// partial specialization: both template parameters have same type
template<typename T>
class MyClass<T,T> {
...
};
// partial specialization: second type is int
template<typename T>
class MyClass<T,int> {
...
};
// partial specialization: both template parameters are pointer types
template<typename T1, typename T2>
class MyClass<T1*,T2*> {
...
};
MyClass<int,float> mif; // uses MyClass<T1,T2>
MyClass<float,float> mff; // uses MyClass<T,T>
MyClass<float,int> mfi; // uses MyClass<T,int>
MyClass<int*,float*> mp; // uses MyClass<T1*,T2*>
MyClass<int,int> m; // ERROR: matches MyClass<T,T>
// and MyClass<T,int>
MyClass<int*,int*> m; // ERROR: matches MyClass<T,T>
// and MyClass<T1*,T2*>
解决最后一个二义性的问题,可以提供两个类型相同指针的偏特化:
template<typename T>
class MyClass<T*,T*> {
...
};
类模板默认参数
和函数模板类似,类模板也可以指定默认模板参数。例如:
template<typename T, typename Cont = std::vector<T>>
class Stack {
public:
void push(const T& x);
void pop();
const T& top() const;
bool empty() const;
private:
Cont v;
};
template<typename T, typename Cont>
void Stack<T, Cont>::push(const T& x)
{
v.emplace_back(x);
}
template<typename T, typename Cont>
void Stack<T, Cont>::pop()
{
assert(!v.empty());
v.pop_back();
}
template<typename T, typename Cont>
const T& Stack<T,Cont>::top() const
{
assert(!v.empty());
return v.back();
}
template<typename T, typename Cont>
bool Stack<T, Cont>::empty() const
{
return v.empty();
}
int main()
{
Stack<int> intStack;
intStack.push(1);
std::cout << intStack.top(); // 1
intStack.pop();
Stack<double, std::deque<double>> doubleStack;
doubleStack.push(3.14);
std::cout << doubleStack.top(); // 3.14
dblStack.pop();
}
类型别名
Typedefs 和别名申明(Alias Declarations)
- 使用
typedef
typedef Stack<int> IntStack; // typedef
void foo (IntStack const& s); // s is stack of ints
IntStack istack[10]; // istack is array of 10 stacks of ints
- 使用
using
(C++11)
using IntStack = Stack<int>; // alias declaration
void foo (IntStack const& s); // s is stack of ints
IntStack istack[10]; // istack is array of 10 stacks of ints
别名模板
别名申明(Alias declarations)还可以模板化,被称为别名模板(Alias Templates)。
template<typename T>
using DequeStack = Stack<T, std::deque<T>>;
DequeStack<int>
和 Stack<int, std::deque<int>>
代表相同的类型。
成员类型的别名模板
template<typename T>
struct MyType {
using iterator = typename std::vector<T>::iterator;
};
template<typename T>
using MyTypeIterator = typename MyType<T>::iterator;
MyTypeIterator<int> pos;
C++14 标准库的 type traits
就使用了上述方法定义了类型简称。
typename std::add_const<T>::type // C++11
std::add_const_t<T> // C++14
namespace std {
template<typename T> using add_const_t = typename add_const<T>::type;
}
类模板参数推导
C++17 之前,你必须显示指定类模板的参数(除非有默认值),但是 C++17 之后,如果构造函数可以推导出模板参数,则可以不指定。
Stack<int> intStack1; // stack of strings
Stack<int> intStack2 = intStack1; // OK in all versions
Stack intStack3 = intStack1; // OK since C++17
通过提供初始化参数的构造函数,编译器可以借助它进行模板参数的推导。例如:
template<typename T>
class Stack {
public:
Stack(T const& elem) : elems({elem}) {}
private:
std::vector<T> elems;
};
Stack intStack = 0; // Stack<int> deduced since C++17
原则上,你也可以使用一个字面值字符串初始化 Stack
。
Stack stringStack = "bottom"; // Stack<char const[7]> deduced since C++17
但这里有点问题:用引用传递模板类型 T
的实参时,模板参数不会 decay
,最终得到的类型是原始数组类型 Stack<char const[7]>
,导致我们不能 push 一个不同大小的字符串到 Stack 中。
传值的话则不会有这种问题,模板实参会 decay
,原始的数组类型会转换为指针。
template<typename T>
class Stack {
public:
Stack(T elem) : elems({elem}) {}
private:
std::vector<T> elems;
};
Stack stringStack = "bottom"; // Stack<const char*> deduced since C++17
并且,这里最好使用 std::move 避免不必要的拷贝。
template<typename T>
class Stack {
public:
Stack(T elem) : elems({std::move(elem)}) {}
private:
std::vector<T> elems;
};
Deduction Guides
为了禁止为容器类推断原始字符指针,可以为模板参数推导提供推导指南(Deduction Guides)。例如,当传入字符串字面值或 C 风格的字符串时,Stack
被实例化为 std::string
类型:
Stack(char const*) -> Stack<std::string>; // 一般紧跟在类的定义后面
Stack stringStack{"bottom"}; // OK: Stack<std::string> deduced since C++17
但是,下面的这种方式仍然无法工作:
Stack stringStack = "bottom"; // Stack<std::string> deduced, but still not valid
因为 Stack
被实例化为:
class Stack {
public:
Stack(const std::string& elem) : elems({elem}) {}
private:
std::vector<std::string> elem;
};
符串字面值常量类型为 const char[7]
,而构造函数期望的是 std::string
,无法拷贝初始化。还是需要如下初始化:
Stack stringStack{"bottom"};
下面都是调用了拷贝构造函数初始化 Stack
:
Stack s1(stringStack); // Stack<std::string> deduced
Stack s2{stringStack}; // Stack<std::string> deduced
Stack s3 = {stringStack}; // Stack<std::string> deduced
模板化聚合类
聚合类(Aggregate classes)(所有成员都是public、没有定义构造函数、没有类内初始值、没有基类、也没有虚函数)也可以模板化:
template<typename T>
struct ValueWithComment {
T value;
std::string comment;
};
ValueWithComment<int> vc;
vc.value = 42;
vc.comment = "initial value";
C++17 可以为聚合类指定模板参数推导指南。例如:
ValueWithComment(char const*, char const*)
-> ValueWithComment<std::string>;
ValueWithComment vc2 = {"hello", "initial value"};
没有推导指南,聚合类无法初始化,因为聚合类没有构造函数。标准库 std::array<>
也是一个聚合类,C++ 标准库也是为它指定了模板参数推导指南。
以上,就是本文的全部内容,更多的模板知识,尽情期待!
参考
- http://www.tmplbook.com