类模板示例
template <typename T>
class Stack
{
public:
Stack();
Stack(const Stack<T> &); // T是同一类型的类模板才能拷贝
Stack<T> &operator=(const Stack<T> &);
void push(const T &);
void pop();
const T &top() const;
bool empty() const;
private:
std::vector<T> v;
};
template <typename T>
Stack<T>::Stack()
{
}
template <typename T>
Stack<T>::Stack(const Stack<T> &rhs) : v(rhs.v)
{
}
template <typename T>
Stack<T> &Stack<T>::operator=(const Stack<T> &rhs)
{
v = rhs.v;
return *this;
}
template <typename T>
void Stack<T>::push(const T &x)
{
v.emplace_back(x);
}
template <typename T>
void Stack<T>::pop()
{
assert(!v.empty());
v.pop_back();
}
template <typename T>
const T &Stack<T>::top() const
{
assert(!v.empty());
return v.back();
}
template <typename T>
bool Stack<T>::empty() const
{
return v.empty();
}
int main()
{
using IntStack = Stack<int>; // typedef Stack<int> IntStack
IntStack intStack; // Stack<int> intStack
intStack.push(42);
std::cout << intStack.top(); // 42
Stack<std::string> stringStack;
stringStack.push("hi");
std::cout << stringStack.top(); // hi
stringStack.pop();
}
模板实参可以是任何类型
Stack<double*> doublePtrStack;
Stack<Stack<int>> intStackStack;
成员函数只有被调用到时才实例化
- 如果类模板有static数据成员,每种实例化类型都会实例化static数据成员。static成员函数和数据成员只被同类型共享
```cpp
template
class A { public: static std::size_t count(); private: static std::size_t n; };
template
A
<a name="dbc254ac"></a>
## 类模板的部分实例化
- 由于成员函数只有被调用到时才实例化,模板实参只要提供必要的操作,而非所有需要的操作。如Stack提供一个printOn对每个元素调用`operator<<`,即使没有对元素定义`operator<<`也能使用这个类。只有调用printOn时才会产生错误,因为这时不能对这些元素实例化`operator<<`
```cpp
template<typename T>
class Stack {
...
void printOn(std::ostream&) const;
};
template<typename T>
void Stack<T>::printOn(std::ostream& os) const
{
for (const T& x : v) os << x << ' ';
}
Stack<std::pair<int, int>> s; // std::pair没有定义operator<<
s.push({1, 2}); // OK
s.push({3, 4}); // OK
std::cout << s.top().first << s.top().second; // 34
s.printOn(std::cout); // 错误:元素类型不支持operator<<
友元非成员函数
与其使用printOn函数打印元素,不如重载
operator<<
,然而通常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; } };
如果在类外定义友元,类模板参数不可见,事情会复杂很多 ```cpp template
class Stack { … friend std::ostream& operator<<(std::ostream&, const Stack ); };
std::ostream& operator<<(std::ostream& os,
const Stack
- 有两个解决方案,一是**隐式声明一个新的函数模板,并使用不同的模板参数**
```cpp
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;
}
- 二是将友元前置声明为模板,而友元参数中包含类模板,这样就必须先前置声明类模板
```cpp
template
// operator<<中参数中要求Stack模板可见 class Stack;
template
// 随后就可以将其声明为友元
template
// 类外定义
template
- 同样,函数只有被调用到时才实例化,元素没有定义`operator<<`时也可以使用这个类,只有调用`operator<<`时才会出错
```cpp
Stack<std::pair<int, int>> s; // std::pair没有定义operator<<
s.push({1, 2}); // OK
s.push({3, 4}); // OK
std::cout << s.top().first << s.top().second; // 34
std::cout << s << '\n'; // 错误:元素类型不支持operator<<
特化与偏特化
特化:模板参数不指定,直接将类型T替换成具体的类型:
template<>
class Stack<std::string> {
public:
void push(const std::string&);
void pop();
std::string const& top() const;
bool empty() const;
private:
std::deque<std::string> v;
};
void Stack<std::string>::push(const std::string& x)
{
v.emplace_back(x);
}
void Stack<std::string>::pop()
{
assert(!v.empty());
v.pop_back();
}
const std::string& Stack<std::string>::top() const
{
assert(!v.empty());
return v.back();
}
bool Stack<std::string>::empty() const
{
return v.empty();
}
偏特化:指定模板参数,不直接使用T类型,内部使用的类型T的特殊形式或其他组合形式:
// 针对指针类型的偏特化
template<typename T>
class Stack<T*> {
public:
void push(T*);
T* pop();
T* top() const;
bool empty() const;
private:
std::vector<T*> v;
};
template<typename T>
void Stack<T*>::push(T* x)
{
v.emplace_back(x);
}
template<typename T>
T* Stack<T*>::pop()
{
assert(!v.empty());
T* p = v.back();
v.pop_back();
return p;
}
template<typename T>
T* Stack<T*>::top() const
{
assert(!v.empty());
return v.back();
}
template<typename T>
bool Stack<T*>::empty() const
{
return v.empty();
}
特化可以提供一个轻微不同的实现,比如这里的pop返回存储的指针,所以当指针用new创建时,类模板的用户能delete
Stack<int*> s; s.push(new int{42}); std::cout << *s.top(); delete s.pop();
类模板也能特化多个模板参数之间的关系 ```cpp template
class A {};
// 偏特化:两个模板参数有相同类型
template
// 偏特化:第二个模板参数类型为int
template
// 偏特化:两个模板参数都是指针类型
template
A
**注意**:多个偏特化匹配程度相同时,将产生二义性错误
```cpp
A<int, int> e; // 错误:同时匹配A<T, T>和A<T, int>
A<int*, int*> f; // 错误:同时匹配A<T, T>和A<T1*, T2*>
要解决第二个二义性错误,可以再提供一个两个相同指针类型的偏特化
template<typename T>
class A<T*, T*>
{};
类模板默认实参
- 类模板也可以指定默认的模板实参
```cpp
template
> class Stack { public: void push(const T& x); void pop(); const T& top() const; bool empty() const; private: Cont v; };
template
template
template
template
int main()
{
Stack
Stack
<a name="51c4a473"></a>
## 类型别名
```cpp
using IntStack = Stack<int>; // typedef Stack<int> IntStack;
void f(const IntStack&);
IntStack s[10]; // 元素为10个Stack<int>的数组
- 别名模板在定义类模板成员的类型简称时十分有用
```cpp
template
struct MyType { using iterator = typename std::vector ::iterator; };
template
// 对于下面这个使用
typename MyType
- C++14中用这种方法为所有的[type traits](https://zh.cppreference.com/w/cpp/header/type_traits)定义了简称
```cpp
// C++11中的
typename std::add_const<T>::type
// 在C++14中可以简写为
std::add_const_t<T>
// 标准库的定义中为
namespace std {
template<typename T>
using add_const_t = typename add_const<T>::type;
}
类模板实参推断
C++17开始,如果构造函数能推断出所有模板参数(没有默认值),就不用显式指定模板实参
Stack<int> s1; Stack<int> s2 = s1; // OK in all versions Stack s3 = s1; // OK since C++17
提供一个传递初始化实参的构造函数,可支持单个元素类型的推断 ```cpp template
class Stack { public: Stack() = default; Stack(const T& x) : v({x}) {} // 单元素初始化 private: std::vector v; };
Stack intStack = 0; // Stack
- 原则上也可以传递字符串字面值常量,但这样会造成许多麻烦。用引用传递模板类型T的实参时,模板参数不会decay,最终得到的类型是原始数组类型
```cpp
Stack stringStack = "bottom"; // Stack<char const[7]> deduced since C++17
- 传值的话则不会有这种问题,模板实参会decay,原始数组类型会转换为指针
```cpp
template
class Stack { public: Stack(T x) : v({x}) {} private: std::vector v; };
Stack stringStack = “bottom”; // Stack
- 传值时最好使用[std::move](https://zh.cppreference.com/w/cpp/utility/move)以避免不必要的拷贝
```cpp
template<typename T>
class Stack {
public:
Stack(T x) : v({std::move(x)}) {}
private:
std::vector<T> v;
};
除了传值,还有一种方法是禁止为容器类推断原始字符指针,可以定义deduction guide来提供对现有模板实参额外的推断,这样传递的字符串字面值常量或C风格字符串都将实例化为std::string
Stack(const char*) -> 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
因为推断std::string实例化了一个
Stack<std::string>
如下class Stack { public: Stack(const std::string& x) : v({x}) {} private: std::vector<std::string> v; };
字符串字面值常量类型为const char[7],而构造函数期望的是std::string,所以必须初始化如下
Stack stringStack{"bottom"}; // Stack<std::string> deduced and valid
注意,下列初始化都调用拷贝构造函数声明相同类型,而不是初始化一个元素是stringStack的stack
Stack s1(stringStack); // Stack<std::string> deduced Stack s2{stringStack}; // Stack<std::string> deduced Stack s3 = {stringStack}; // Stack<std::string> deduced
模板化聚合
聚合类也能作为模板
template<typename T> struct A { T x; std::string s; };
这样可以为了参数化值而定义一个聚合,它可以像其他类模板一样声明对象,同时当作一个聚合使用
A<int> a; a.x = 42; a.s = "initial value";
C++17中可以为聚合类模板定义deduction guide ```cpp template
struct A { T x; std::string s; };
A(const char, const char) -> A
int main() { A a = { “hi”, “initial value” }; std::cout << a.x; // hi }
- 没有deduction guide,初始化就无法进行,因为A没有构造函数来推断。[std::array](https://zh.cppreference.com/w/cpp/container/array)也是一个聚合,元素类型和大小都是参数化的,C++17为其定义了一个deduction guide
```cpp
namespace std {
template<typename T, typename... U> array(T, U...)
-> array<enable_if_t<(is_same_v<T, U> && ...), T>, (1 + sizeof...(U))>;
}
std::array a{ 1, 2, 3, 4 };
// 等价于
std::array<int, 4> a{ 1, 2, 3, 4 };