美文网首页
02 类模板

02 类模板

作者: 奇点创客 | 来源:发表于2021-05-20 13:41 被阅读0次

    类模板示例

    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成员函数和数据成员只被同类型共享
    template<typename T>
    class A {
     public:
      static std::size_t count();
     private:
      static std::size_t n;
    };
    
    template<typename T>
    std::size_t A<T>::n = 0;
    
    A<std::string> a; // 实例化A<std::string>::n
    A<int> b, c, d; // 实例化A<int>::n,bcd共享A<int>::count()和A<int>::n
    std::size_t n = A<int>::count(); // 实例化A<int>::count()
    n = b.count(); // 使用A<int>::count()
    n = A::count(); // 错误:必须指定模板参数,否则无法得知实例化版本
    

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

    • 由于成员函数只有被调用到时才实例化,模板实参只要提供必要的操作,而非所有需要的操作。如Stack提供一个printOn对每个元素调用operator<<,即使没有对元素定义operator<<也能使用这个类。只有调用printOn时才会产生错误,因为这时不能对这些元素实例化operator<<
    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;
      }
    };
    
    • 如果在类外定义友元,类模板参数不可见,事情会复杂很多
    template<typename T> 
    class Stack {
      ...
      friend std::ostream& operator<<(std::ostream&, const Stack<T>);
    };
    
    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>> 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<<
    

    类模板特化

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

    偏特化(Partial Specialization)

    // 针对指针类型的偏特化
    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();
    
    • 类模板也能特化多个模板参数之间的关系
    template<typename T1, typename T2> 
    class A
    {};
    
    // 偏特化:两个模板参数有相同类型
    template<typename T> 
    class A<T, T>
    {};
    
    // 偏特化:第二个模板参数类型为int
    template<typename T> 
    class A<T, int>
    {};
    
    // 偏特化:两个模板参数都是指针类型
    template<typename T1, typename T2> 
    class A<T1*, T2*>
    {};
    
    A<int, double> a; // A<T1, T2>
    A<double, float> b; // A<T, T>
    A<double, int> c; // A<T, int>
    A<int*, double*> d; // A<T1*, T2*>
    
    • 多个偏特化匹配程度相同时,将产生二义性错误
    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*>
    {};
    

    类模板默认实参

    • 类模板也可以指定默认的模板实参
    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();
    }
    

    类型别名

    using IntStack = Stack<int>; // typedef Stack<int> IntStack;
    void f(const IntStack&);
    IntStack s[10]; // 元素为10个Stack<int>的数组
    
    • 别名模板在定义类模板成员的类型简称时十分有用
    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> s1;
    Stack<int> s2 = s1; // OK in all versions
    Stack s3 = s1; // OK since C++17
    
    • 提供一个传递初始化实参的构造函数,可支持单个元素类型的推断
    template<typename T> 
    class Stack {
     public:
      Stack() = default;
      Stack(const T& x) : v({x}) {} // 单元素初始化
     private:
      std::vector<T> v;
    };
    
    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 {
     public:
      Stack(T x) : v({x}) {}
     private:
      std::vector<T> v;
    };
    
    Stack stringStack = "bottom"; // Stack<const char*> deduced since C++17
    
    • 传值时最好使用std::move以避免不必要的拷贝
    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
    

    模板化聚合(Templatized Aggregates)

    • 聚合类也能作为模板
    template<typename T> 
    struct A {
      T x;
      std::string s;
    };
    
    • 这样可以为了参数化值而定义一个聚合,它可以像其他类模板一样声明对象,同时当作一个聚合使用
    A<int> a;
    a.x = 42;
    a.s = "initial value";
    
    • C++17中可以为聚合类模板定义deduction guide
    template<typename T> 
    struct A {
      T x;
      std::string s;
    };
    
    A(const char*, const char*) -> A<std::string>;
    
    int main()
    {
      A a = { "hi", "initial value" };
      std::cout << a.x; // hi
    }
    
    • 没有deduction guide,初始化就无法进行,因为A没有构造函数来推断。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{ 1, 2, 3, 4 };
    // 等价于
    std::array<int, 4> a{ 1, 2, 3, 4 };
    

    相关文章

      网友评论

          本文标题:02 类模板

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