当前位置:网站首页>Core knowledge of C + + 11-17 template (2) -- class template

Core knowledge of C + + 11-17 template (2) -- class template

2020-11-08 23:46:00 Zhang Yachen

Class template declaration 、 Implement and use

Statement :

template <typename T> 
class Stack {
private:
  std::vector<T> elems; // elements
public:
  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 is empty
    return elems.empty();
  }
};

Realization :

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

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

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

Use :

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

There are two points to note

  • The constructor inside the class declaration 、 copy constructor 、 Destructor 、 Assignment, etc. where class names are used , Can be Stack<T> Shorthand for Stack, for example :
template<typename T>
class Stack {
  ...
  Stack (Stack const&);                           // copy constructor
  Stack& operator= (Stack const&);      // assignment operator
...
};

But outside the class , Still need Stack<T>:

template<typename T>
bool operator== (Stack<T> const& lhs, Stack<T> const& rhs);
  • Class templates cannot be declared or defined in function or block scope . Generally, class templates can only be defined in global/namespace Scope , Or in the declaration of other classes .

Class Instantiation

instantiation The concept of is said in the function template . In class templates , Class template functions are only called when they are called instantiate. In the example above ,push() and top() Will be Stack<int> and Stack<std::string> the instantiate, however pop() Only be Stack<std::string> the instantiate.

image

Use part of the member function of the class template

We are Stack New offer printOn() function , This needs to be elem Support << operation :

template<typename T>
class Stack {
...
    void printOn() (std::ostream& strm) const {
        for (T const& elem : elems) {
             strm << elem << ' ';           // call << for each element
         }
    }
};

According to the previous section about class templates instantiation, Only when the function is used will the function be executed instantiation. If our template parameter is an element, it does not support << Of std::pair< int, int>, You can still use other functions of the class template , Only a call printOn It's only when you make a mistake :

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

Concept

This raises a question , How do we know what operations a class template and its template functions need ?

stay c++11 in , We have static_assert:

template<typename T>
class C
{
    static_assert(std::is_default_constructible<T>::value, "Class C requires default-constructible elements");
...
};

If do not have static_assert, The template parameters provided do not satisfy std::is_default_constructible, The code doesn't compile well . But the error messages generated by the compiler can be very long , Contains the entire template instantiation Information about —— From the beginning instantiation Until the wrong place , It's hard to find out the real cause of the mistake .

So use static_assert It's a way . however static_assert For making simple judgments , In the actual scenario, our scenario will be more complex , For example, to determine whether a template parameter has a specific member function , Or ask them to support comparison , Use... In this case concept It's more appropriate .

concept yes c++20 A feature used to indicate template library constraints in , It will be explained separately later concept, For the sake of the length of the article, I'll just talk about why there should be concept.

Friends

First of all, it needs to be clear : Although friend looks like a member of this class , But friends don't belong to this category . Here friend refers to friend function and friend class . This is essential for understanding the following grammatical rules .

Mode one

template<typename T>
class Stack {
  ...
  void printOn(std::ostream &strm) const {
    for (T const &elem : elems) {
      strm << elem << ' '; // call << for each element
    }
  }

  template <typename U>
  friend std::ostream &operator<<(std::ostream &, Stack<U> const &);
};

template <typename T>
std::ostream &operator<<(std::ostream &strm, Stack<T> const &s) {
  s.printOn(strm);
  return strm;
}


int main() {
  Stack<std::string> s;
  s.push("hello");
  s.push("world");

  std::cout << s;

  return 0;
}

Here, the friend function declared in the class uses template parameters different from the class template <template typename U>, Because the template parameters of friend function and class template do not affect each other , This can be understood as creating a new function template .

Another example of a friend class :

template<typename T>
class foo {
  template<typename U>
  friend class bar;
};

Different template parameters are also used here . That is to say :bar<char>bar<int>bar<float> And any other type of bar All are foo<char> Friends .

Mode two

template<typename T>
class Stack;
template<typename T>
std::ostream& operator<< (std::ostream&, Stack<T> const&);

template<typename T>
class Stack {
  ...
  friend std::ostream& operator<< <T> (std::ostream&, Stack<T> const&);
};

It's stated here in advance that Stack And operator<<, And in the class template , operator<< I used <T>, No new template parameters are used . Compared with the first way , Here we create a special non member function template as a friend ( Notice the declaration of this friend function , It's not <T> Of ).

The example of the second friend class in mode 1 is written in this way as follows :

template<typename T>
class bar;

template<typename T>
struct foo {
  friend class bar<T>;
};

In contrast to , There is only bar<char> yes foo<char> Friends class .

There are many friend rules for class templates , It's OK to know what kind of rules there are (Friend Classes of Class Templates、Friend Functions of Class Templates、Friend Templates), When you use it, you can check it again . You can refer to :《C++ Templates Second Edition》12.5 Section . ( Official account : Hong Chen smiles . reply : e-book obtain pdf)

Full specialization of class template

Similar to function templates , But here's the thing , If you want to fully specialize a class template , You have to fully specialize all the member functions of this class template .

template <> 
class Stack<std::string> {
private:
  std::deque<std::string> elems; // elements
public:
  void push(std::string const &); // push element
  void pop();                     // pop element
  std::string const &top() const; // return top element
  bool empty() const {            // return whether the stack is empty
    return elems.empty();
  }
};
void Stack<std::string>::push(std::string const &elem) {
  elems.push_back(elem); // append copy of passed elem
}

void Stack<std::string>::pop() {
  assert(!elems.empty());
  elems.pop_back(); // remove last element
}

std::string const &Stack<std::string>::top() const {
  assert(!elems.empty());
  return elems.back(); // return copy of last element
}

At the beginning of the class declaration , Need to use template<> It also indicates the type of the fully specialized parameter of the class template :

template<>
class Stack<std::string> {
...
};

In member functions , Need to put T Replace with a specialized parameter type :

void Stack<std::string>::push (std::string const& elem) {
  elems.push_back(elem); // append copy of passed elem
}

Partial specialization of class templates

Class templates can be partially specialized for some feature scenarios , For example, we make partial specialization for template parameter is pointer :

// partial specialization of class Stack<> for pointers:
template <typename T> 
class Stack<T *> {
private:
  std::vector<T *> elems; // elements
public:
  void push(T *);      // push element
  T *pop();            // pop element
  T *top() const;      // return top element
  bool empty() const { // return whether the stack is empty
    return elems.empty();
  }
};

template <typename T> 
void Stack<T *>::push(T *elem) {
  elems.push_back(elem); // append copy of passed elem
}

template <typename T> 
T *Stack<T *>::pop() {
  assert(!elems.empty());
  T *p = elems.back();
  elems.pop_back(); // remove last element
  return p;         // and return it (unlike in the general case)
}

template <typename T> 
T *Stack<T *>::top() const {
  assert(!elems.empty());
  return elems.back(); // return copy of last element
}

Notice the difference between class declaration and full specialization :

template<typename T>
class Stack<T*> {
};

Use :

Stack<int*> ptrStack; // stack of pointers (special implementation)
ptrStack.push(new int{42});

Partial specialization of multi template parameters

Similar to function template overloading , It's easy to understand .

The original template :

template<typename T1, typename T2>
class MyClass {
...
};

heavy load :

// 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*> {
...
};

Use :

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*>

There will also be overload conflicts :

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*>

image

Default template parameters

It's also similar to the default parameter of a function . For example, we do it for Stack<> Add a default parameter , On behalf of management Stack The container type of the element :

template <typename T, typename Cont = std::vector<T>> 
class Stack {
private:
  Cont elems; // elements
public:
  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 is empty
    return elems.empty();
  }
};

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

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

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

Notice that the template parameter that defines the member function becomes 2 individual :

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

Use :

// stack of ints:
Stack<int> intStack;

// stack of doubles using a std::deque<> to manage the elements
Stack<double,std::deque<double>> dblStack;

Type Aliases

new name for complete type

Two ways :typedef、using(c++11)

  • typedef
typedef Stack<int> IntStack; 
void foo (IntStack const& s);
IntStack istack[10]; 
  • using
using IntStack = Stack<int>; 
void foo (IntStack const& s); 
IntStack istack[10];

alias template

using Than typedef One big advantage is that you can define alias template:

template <typename T>
using DequeStack = Stack<T, std::deque<T>>; // stack of strings

int main() {
  DequeStack<int> ds;

  return 0;
}

Just a little bit more , Class templates cannot be declared or defined in function or block scope . Generally, class templates can only be defined in global/namespace Scope , Or in the declaration of other classes .

In the previous function template article std::common_type_t, It's actually an alias :

template <class ..._Tp> using common_type_t = typename common_type<_Tp...>::type;

Alias Templates for Member Types

  • typedef:
struct C {
  typedef ... iterator;
  ...
};
  • using:
struct MyType {
  using iterator = ...;
  ...
};

Use :

template<typename T>
using MyTypeIterator = typename MyType<T>::iterator;       // typename There has to be 
MyTypeIterator<int> pos;

keyword typename

The note above says :typename MyType<T>::iterator Inside typename Is a must , Because of the typename Represents a type defined within a class , otherwise ,iterator It will be treated as a static variable or an enumeration :

template <typename T> class B {
public:
  static int x;                 //  Static variables within a class      
  using iterator = ...;     //  Types defined within a class 
};

template <typename T>
int B<T>::x = 20;

int main() {

  std::cout << B<int>::x;     // 20

  return 0;
}

Using or Typedef

Individuals tend to use using :

  • using Use =, More in line with the habit of reading code 、 Clearer :
typedef void (*FP)(int, const std::string&);       // typedef

using FP = void (*)(int, const std::string&);       // using
  • As mentioned above ,using Definition alias template More convenient .

image

Parameter derivation of class template Class Template Argument Deduction

You may feel that the type of template parameter that you need to display every time you use a template is superfluous , If a class template can look like auto Just like automatically deriving template types . stay C++17 in , The idea became possible : If the constructor can deduce all the template parameters , Then we don't need to show the template parameter type .

Stack<int> intStack1; // stack of strings
Stack<int> intStack2 = intStack1; // OK in all versions
Stack intStack3 = intStack1; // OK since C++17

Add constructors that infer class model types :

template<typename T>
class Stack {
  private:
      std::vector<T> elems; // elements
  public:
      Stack () = default;       // 
      Stack (T elem) : elems({std::move(elem)}) {}
...
};

Use :

Stack intStack = 80;      // Stack<int> deduced since C++17

Why add Stack () = default; In order to Stack<int> s; This default structure does not report errors .

Deduction Guides

We can use Deduction Guides To provide additional template parameter derivation rules , Or modify the existing template parameter inference rules .

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

Stack stringStack{"bottom"};           // OK: Stack<std::string> deduced since C++17

More rules and usage can be found in :Class template argument deduction (CTAD) (since C++17)

( End )

Friends can pay attention to my official account , Get the most up-to-date updates :

版权声明
本文为[Zhang Yachen]所创,转载请带上原文链接,感谢