美文网首页C++ Templates
【C++ Templates(2)】类模板

【C++ Templates(2)】类模板

作者: downdemo | 来源:发表于2018-04-03 10:23 被阅读62次

类模板示例

// basics/stack1.hpp

#include <vector>
#include <cassert>

template <typename T>
class Stack {
private:
    std::vector<T> elems;
public:
    Stack();
    Stack(const Stack<T>&); // T是同一类型的类模板才能拷贝
    Stack<T>& operator=(const Stack<T>&);
    void push(const T& elem);
    void pop();
    const T& top() const;
    bool empty() const;
};

template<typename T>
Stack<T>::Stack()
{}

template<typename T>
Stack<T>::Stack(const Stack<T>& stack) : elems(stack.elems)
{}

template<typename T>
Stack<T>& Stack<T>::operator=(const Stack<T>& rhs)
{
    elems = rhs.elems;
    return *this;
}

template<typename T>
void Stack<T>::push(const T& elem)
{
    elems.push_back(elem);
}

template<typename T>
void Stack<T>::pop()
{
    assert(!elems.empty());
    elems.pop_back();
}

template<typename T>
const T& Stack<T>::top () const
{
    assert(!elems.empty());
    return elems.back();
}

template<typename T>
bool Stack<T>::empty() const
{
    return elems.empty();
}

使用类模板

// basics/stack1test.cpp

#include "stack1.hpp"
#include <iostream>
#include <string>

int main()
{
    using IntStack = Stack<int>; // typedef Stack<int> IntStack
    IntStack intStack; // Stack<int> intStack
    Stack<std::string> stringStack;
    intStack.push(7);
    std::cout << intStack.top() << '\n'; // 7
    stringStack.push("hello");
    std::cout << stringStack.top() << '\n'; // hello
    stringStack.pop();
}
  • 模板实参可以是任何类型
Stack<float*> floatPtrStack;
Stack<Stack<int>> intStackStack;
  • 成员函数只有被调用到时才实例化
  • 如果类模板有static数据成员,实例化的每种类型都会实例化static数据成员。static成员函数和数据成员只被同类型共享
template<typename T>
class A
{
public:
    static std::size_t count();
private:
    static std::size_t ctr;
};

template<typename T>
size_t A<T>::ctr = 0;

A<std::string> a; // 实例化A<std::string>::ctr
A<int> b, c, d; // 实例化A<int>::ctr,bcd共享A<int>::count和A<int>::ctr
auto ct = A<int>::count(); // 实例化A<int>::count
ct = b.count(); // 使用A<int>::count
ct = A::count(); // 错误:必须指定模板参数,否则无法得知实例化版本

类模板的部分使用(partial usage)

  • 由于成员函数只有被调用到时才实例化,模板实参只要提供必要的操作,而非所有需要的操作。如Stack提供一个printOn对每个元素调用operator<<,即使没有对元素定义operator<<也能使用这个类。只有调用printOn时才会产生错误,因为这时不能对这些元素实例化operator<<
template<typename T> 
class Stack {
    ...
    void printOn(std::ostream& os) const;
};

template <typename T>
void Stack<T>::printOn(std::ostream& os) const
{
    for (const T& x : elems) os << x << ' ';
}

Stack<std::pair<int, int>> ps; // std::pair没有定义operator<<
ps.push({1, 2}); // OK
ps.push({3, 4}); // OK
std::cout << ps.top().first << '\n'; // 3
std::cout << ps.top().second << '\n'; // 4
ps.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;
    }
};
  • 如果在类外定义友元,类模板参数不可见,事情会复杂很多
template<typename T> 
class Stack {
    ...
    friend std::ostream& operator<< (std::ostream& os, const Stack<T>& stack);
};

std::ostream& operator<<(std::ostream& os,
    const Stack<T>& stack) // 错误:类模板参数T不可见
{
    stack.printOn(os);
    return os;
}
  • 有两个解决方案,一是隐式声明一个新的函数模板,并使用不同的模板参数
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>&);
};

// 类外定义
template<typename T>
std::ostream& operator<<(std::ostream& os, const Stack<T>& stack)
{
    stack.printOn(os);
    return os;
}
  • 同样,函数只有被调用到时才实例化,元素没有定义operator<<时也可以使用这个类,只有调用operator<<时才会出错
Stack<std::pair<int, int>> ps; // std::pair没有定义operator<<
ps.push({1, 2}); // OK
ps.push({3, 4}); // OK
std::cout << ps.top().first << '\n'; // 3
std::cout << ps.top().second << '\n'; // 4
std::cout << ps << '\n'; // 错误:元素类型不支持operator<<

类模板特化

// basics/stack2.hpp

#include "stack1.hpp"
#include <deque>
#include <string>
#include <cassert>

template<>
class Stack<std::string> {
private:
    std::deque<std::string> elems;
public:
    void push(const std::string&);
    void pop();
    std::string const& top() const;
    bool empty() const;
};

void Stack<std::string>::push (const std::string& 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();
}

bool Stack<std::string>::empty () const
{
    return elems.empty();
}

局部特化(Partial Specialization)

// basics/stackpartspec.hpp

#include  "stack1.hpp"
// 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;
};

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

template<typename T>
bool Stack<T*>::empty() const
{
    return elems.empty();
}
  • 特化可以提供一个轻微不同的实现,比如这里的pop()返回存储的指针,所以当指针用new创建时,类模板的用户能delete
Stack<int*> ptrStack;
ptrStack.push(new int{42});
std::cout << *ptrStack.top() << '\n'; 
delete ptrStack.pop();
  • 类模板也能特化多个模板参数之间的关系,对如下类模板
template<typename T1, typename T2> 
class MyClass
{};
  • 可局部特化如下
// 局部特化:两个模板参数有相同类型
template<typename T> 
class MyClass<T, T>
{};

// 局部特化:第二个模板参数类型为int
template<typename T> 
class MyClass<T, int>
{};

// 局部特化:两个模板参数都是指针类型
template<typename T1, typename T2> 
class MyClass<T1*, T2*>
{};

MyClass<int, float> A; // uses MyClass<T1, T2>
MyClass<float, float> B; // uses MyClass<T, T>
MyClass<float, int> C; // uses MyClass<T, int>
MyClass<int*, float*> D; // uses MyClass<T1*, T2*>
  • 多个局部特化匹配程度相同时,将产生二义性错误
MyClass<int, int> E; // ERROR: matches MyClass<T, T> and MyClass<T, int>
MyClass<int*, int*> F; // ERROR: matches MyClass<T, T> and MyClass<T1*, T2*>
  • 要解决第二个二义性错误,可以再提供一个两个相同指针类型的局部特化
template<typename T> 
class MyClass<T*,T*>
{};

类模板默认实参

  • 类模板也可以指定默认的模板实参
// basics/stack3.hpp

#include <vector>
#include <cassert>

template<typename T, typename Cont = std::vector<T>>
class Stack { 
private:
    Cont elems;
public: 
    void push(const T& elem);
    void pop();
    const T& top() const;
    bool empty() const;
};

template<typename T, typename Cont>
void Stack<T,Cont>::push (const T& elem)
{
    elems.push_back(elem);
}

template <typename T, typename Cont>
void Stack<T, Cont>::pop()
{
    assert(!elems.empty());
    elems.pop_back();
}

template<typename T, typename Cont>
const T& Stack<T,Cont>::top () const
{
    assert(!elems.empty());
    return elems.back();
}

template <typename T, typename Cont>
bool Stack<T, Cont>::empty() const
{
    return elems.empty();
}

// basics/stack3test.cpp

#include <iostream>
#include <deque>

int main()
{
    Stack<int> intStack;
    intStack.push(1);
    std::cout << intStack.top() << '\n'; // 1
    intStack.pop();

    Stack<double, std::deque<double>> dblStack;
    dblStack.push(3.14);
    std::cout << dblStack.top() << '\n'; // 3.14
    dblStack.pop();
}

类型别名

  • 使用typedef
typedef Stack<int> IntStack;
void foo (IntStack const& s);
IntStack istack[10]; // istack is array of 10 stacks of ints
  • 使用C++11的using
using IntStack = Stack <int>;
void foo (IntStack const& s);
IntStack istack[10]; // istack is array of 10 stacks of ints
  • 别名模板在定义类模板成员的类型简称时十分有用
template<typename T>
struct MyType {
    using iterator = typename std::vector<T>::iterator;
};

template<typename T>
using Iter = typename MyType<T>::iterator;

// 对于下面这个使用
typename MyType<T>::iterator it;
// 可改写为
Iter<int> it;
  • C++14中用这种方法为所有的type traits定义了简称
// 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> intStack1;
Stack<int> intStack2 = intStack1; // OK in all versions
Stack intStack3 = intStack1; // OK since C++17
  • 提供一个传递初始化实参的构造函数,可支持单个元素类型的推断
template<typename T> 
class Stack { 
private:
    std::vector<T> elems;
public:
    Stack () = default;
    Stack (const T& elem) // initialize stack with one element
    : elems({elem})
    {}
};

Stack intStack = 0; // Stack<int> deduced since C++17
  • 原则上也可以传递字符串字面值常量,但这样会造成许多麻烦。用引用传递模板类型T的实参时,模板参数不会decay,最终得到的类型是原始数组类型
Stack stringStack = "bottom"; // Stack<char const[7]> deduced since C++17
  • 传值的话则不会有这种问题,模板实参会decay,原始数组类型会转换为指针
template<typename T> 
class Stack { 
private:
    std::vector<T> elems;
public:
    Stack (T elem)
    : elems({elem})
    {}
};

Stack stringStack = "bottom"; // Stack<const char*> deduced since C++17
  • 传值时最好移动临时变量elem以免进行不必要的拷贝
template<typename T> 
class Stack { 
private:
    std::vector<T> elems;
public:
    Stack (T elem)
    : elems({std::move(elem)})
    {}
};
  • 除了传值,还有一种方法是禁止为容器类推断原始字符指针,可以定义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 { 
private:
    std::vector<std::string> elems;
public:
    Stack (const std::string& elem)
    : elems({elem})
    {}
};
  • 字符串字面值常量类型为const char[7],而构造函数期望的是std::string,所以必须初始化如下
Stack stringStack{"bottom"}; // Stack<std::string> deduced and valid
  • 注意,下列初始化都调用拷贝构造函数声明相同类型,而不是初始化一个元素是string stack的stack
Stack s1(stringStack); // Stack<std::string> deduced
Stack s2{stringStack}; // Stack<std::string> deduced
Stack s3 = {stringStack}; // Stack<std::string> deduced

模板化聚合(Templatized Aggregates)

  • 聚合类也能作为模板
template<typename T> 
struct ValueWithComment {
    T value;
    std::string comment;
};
  • 这样可以为了参数化值而定义一个聚合,它可以像其他类模板一样声明对象,同时当作一个聚合使用
ValueWithComment<int> vc;
vc.value = 42;
vc.comment = "initial value";
  • C++17中可以为聚合类模板定义deduction guide
template<typename T>
struct ValueWithComment {
    T value;
    std::string comment;
};

ValueWithComment(const char*, const char*)->ValueWithComment<std::string>;

int main()
{
    ValueWithComment vc = { "hello", "initial value" };
    std::cout << vc.value; // hello
}
  • 没有deduction guide,初始化就无法进行,因为ValueWithComment没有构造函数来推断。std::array也是一个聚合,元素类型和大小都是参数化的,C++17为其定义了一个deduction guide
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{42,45,77};
// 等价于
std::array<int, 3> a{42,45,77};

相关文章

网友评论

    本文标题:【C++ Templates(2)】类模板

    本文链接:https://www.haomeiwen.com/subject/spkqhftx.html