C + + - deep understanding of template classes

Posted by cyberdesi on Thu, 27 Jan 2022 03:26:09 +0100

First, can a stack be pressed with a pointer?

Of course, write a simple stack here

When it comes to pointers, you may think of strings. We can pass in the following types

string str; The simplest input, thanks to the powerful string class provided by C + +, we can easily complete this operation

char str[size]; C-style string, but it may not be available for some template class methods

char * str = new char[size];

For the class we implemented, at least it is not feasible not to modify the input data

template<class Type>
bool Stack<Type>::push(const Type &t)
{
    if(now < MAX)
    {
        items[now++] = t;
        return true;
    }else{
        return false;
    }
}

We can see that the element pressing the stack is a pointer. Without human intervention, the pointer points to the memory and will not change. It is meaningless for us to input str repeatedly;

The appropriate approach is to let the program provide an array of pointers (generally a two-dimensional array), in which each pointer points to a different string. The task of the stack is to manage pointers, not to create pointers.

//
// Created by JAN on 2022/1/26.
//

#ifndef C___STACK_H
#define C___STACK_H
template <class T>
class Stack
{
private:
    enum { MAX = 10};
    T items[MAX];
    int now;
public:
    Stack() : now(0) {}
    bool push(T & t);
    bool pop();
    bool empyt() const;
    const T& top() const;
};

template <class T>
bool Stack<T>::push(T &t)
{
    if(now < MAX){
        items[now++] = t;
        return true;
    }else
        return false;
}

template<class T>
bool Stack<T>::empyt() const
{
    if(now == 0)
        return true;
    else
        return false;
}

template<class T>
const T &Stack<T>::top() const
{
    //int temp = now-1;
    return items[now-1];
}

template<class T>
bool Stack<T>::pop()
{
    if(now > 0){
        now--;
        return true;
    }else
        return false;
}
#endif //C___STACK_H
//
// Created by JAN on 2022/1/25.
//

#ifndef C___STACK_H
#define C___STACK_H

template<class Type>
class Stack
{
private:
    enum { MAX = 5 };
    Type items[MAX+1];
    //It can also be written in this form, but don't forget to create a new space in the constructor and free up space with a destructor
    //Type *items;
    int now;
public:
    Stack();
    bool empty();
    bool push(const Type & t);
    bool pop();
    Type top();
};
template<class Type>
Stack<Type>::Stack()
{
    now = 0;
}
template<class Type>
bool Stack<Type>::empty()
{
    return now == 0;
}

template<class Type>
bool Stack<Type>::push(const Type &t)
{
    if(now < MAX)
    {
        items[++now] = t;
        return true;
    }else{
        return false;
    }
}

template<class Type>
bool Stack<Type>::pop()
{
    if(now > 0)
    {
        now--;
        return true;
    }else
        return false;
}

template<class Type>
Type Stack<Type>::top()
{
    if(now > 0){
        return items[now];
    }
}

#endif //C___STACK_H

Operation results

You can see the repeated output eee

Recursive use of templates

We know that there is an array template class. The so-called recursion is just a template set template

array< array<int, 3>, 5> arr;

The array in the outer layer contains five # array templates with three int s

It may be difficult to describe as a language, but you can understand it by looking at the source code

#include <iostream>
#include <valarray>
#include <array>
using namespace std;

int main()
{
    array< array<int, 3>, 5> arr;
    for(int i=0;i<5;i++){
        for(int j=0;j<3;j++){
            cin >> arr[i][j];
        }
    }
    for(int i=0;i<5;i++){
        for(int j=0;j<3;j++){
            cout << arr[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}

It can operate like a two-dimensional array

Accept multiple types of templates

Suppose we use a binary to store data, and the two meta types may be the same or different. How do we write a template class

Simply simulate the binary pair. It's very simple. You may write it with your hands

pair.h

#ifndef PAIR_H
#define PAIR_H
//Binary
//template_name <class type_name, class type_name ... some argument>
//We can call the above template class prototype
template <class Type1, class Type2>
class Pair
{
private:
    Type1 t1;
    Type2 t2;
public:
    Pair() = default;
    Pair(const Type1 & _t1, const Type2 & _t2) : t1(_t1), t2(_t2) {}
    const Type1 & first() const;
    const Type2 & second() const;
};

template <class Type1, class Type2>
const Type1 & Pair<Type1, Type2>/*The scope here is the template prototype, which is easy to forget*/::first() const
{
    return t1;
}

template <class Type1, class Type2>
const Type2 & Pair<Type1, Type2>::second() const
{
    return t2;
}

#endif

main.cpp

#include <iostream>
#include "pair.h"
using namespace std;

int main()
{
    Pair<int, double> p(3,4.12);
    cout << p.first() << endl;
    cout << p.second() << endl;
    return 0;
}

Default type template

Just like the default function parameter, if it is not declared, it can automatically become the default value. If the template class is not declared, it will automatically become the default type

template <class Type1=int, class Type2=int>

...

Pair <> p; //Pair<int, int> p;

Concretization of template class

1. Implicit instantiation

Just like the template function, when the template class does not indicate the instantiation object, the compiler will help us automatically instantiate the type we pass in. Implicit instantiations are not generated until the compiler needs the object.

array <int,3> * parr;

This just declares a template class pointer and does not instantiate it.

2. Display instantiation

When a class is declared using the keyword template and indicating the required type, the compiler generates an explicit instantiation of the class declaration. The declaration must be in the namespace of the template.

The following explicitly declares a class of < string, int >

template class class_name <string,int>

In this case, although no object is created or mentioned, the compiler will also generate class declarations (including method definitions). Like implicit instantiation, materialization will also be generated according to the general template.

3. Explicit concretization

Explicit materialization is the definition of a specific type. For some types, we need to have specific template class methods to deal with it. At this time, explicit concretization is used. Explicit concretization is a further supplement to the generality of template classes.

The definition format is as follows

template class class_name <sprcialized-type-name>
{
    do sometings
};

4. Partial concretization

That is to limit the generality of the template. If we write a template class that can only handle integers and don't want floating-point types to use this template, we can do so

template <class Type, int> 
class class_name
{
    some things;
}

In this way, the second type of template class can only be an integer

If there are multiple templates to choose from, the compiler will choose the most specific one

Pay special attention to the template materialization provided for pointers

template <class T>
class A
{
    ...
};

template <class T*>
class A
{
    ...   
};
------------------------------------------------------
A<char *> a;

Without a second template declaration, the compiler changes T to char*

The second is not to change T to char * *, but to char

Member template

Templates can be used as members of structures, classes, or templates. To fully implement the design of STL, this feature must be used. Here is a simple template class nesting

#include <iostream>

using std::endl;
using std::cout;
//Define template class

template <class T>
class beat
{
private:
    template<class V>
    class hold
    {
    private:
        V val;    //Create a type variable
    public:
        //Constructor
        hold(V v=0) : val(v) {}

        //class method`
        void show() const
        {
            cout << val << endl;
        }
        V value() const
        {
            return val;
        }
    };
    //After defining the inner template, we can create template classes within two template classes
    //The beat Class treats these two template classes as members
    hold<T> q; //And the outermost class instantiate a data type
    hold<int> n; //Explicitly instantiate int type

public:
    //class method
    //Constructor is actually calling the constructor in hold
    beat(T t, int i) : q(t), n(i) {}

    template<class U>
    //Here is the template function
    //U u is unique to this template function. T t and t instantiation of large template class are one type
    U blab (U u, T t)
    {
        return (n.value()+q.value()) * u / t;
    }
    void show() const
    {
        q.show();
        n.show();
    }
};
int main()
{
    //beat(T t, int i) : q(t), n(i)
    beat<double> guy(3.5, 3);
    cout << "T was set to double\n";
    guy.show();
    cout << "V was set to T, which is double, then V was set to int\n";
    //Since it is T is double, the following 2.3 is also double if it is changed to 2
    cout << guy.blab(10,2.3) << endl;
    cout << "U was set to int\n";
    cout << guy.blab(10.0,2.3) << endl;
    cout << "U was set to double\n";
    return 0;
}

Take template class as parameter

Template classes can contain either type parameters (type) or non type parameters (int a). Similarly, template classes can also contain parameters that are themselves templates. They are features of C++11 and are used to implement STL

In fact, the recursion of template class takes the template class as a parameter

The grammatical form is easy to understand

Note that there is no comma in the middle

template < /**/template <typename type_name> class/**/ type_name>
class class_name
{
    do something;
};

Where / * * / template < typename type_name > class / * * / is a type, followed by type_name is the parameter

If we want to stack the binary, our previous stack will not work because the binary is also a template class. The previous stack does not support the template class as a type parameter. We can transform the stack to receive the binary template class. On this basis, we can write a new template class and integrate the stack and the binary, This class accepts two template classes as objects. It can also write another template class and accept a stack template class. Two template stack class objects are instantiated in this template class to represent binary.

The second method seems troublesome, but its universality may be better. The third method is simple and easy to implement

Here we use the last form

#include <iostream>
#include "stack.h"
using namespace std;

template <template<class T> class S, class T1, class T2>
class IDE
{
private:
    S<T1> stack1;
    S<T2> stack2;
public:
    IDE() = default;

    //class method
    bool push(T1 & t1, T2 & t2);
    bool pop();
    void top(T1 & t1, T2 & t2) const;
};

template <template<class T> class S, class T1, class T2>
bool IDE<S, T1, T2>::push(T1 &t1, T2 &t2)
{
    return stack1.push(t1) && stack2.push(t2);
}

template <template<class T> class S, class T1, class T2>
bool IDE<S, T1, T2>::pop()
{
    return stack1.pop() &&stack2.pop();
}

template <template<class T> class S, class T1, class T2>
void IDE<S, T1, T2>::top(T1 &t1, T2 &t2) const
{
    t1 = stack1.top();
    t2 = stack2.top();
}
int main()
{
    IDE<Stack/*no Stack <argument>*/,int ,double> stk;
    int a;
    double b;
    cin >> a >> b;
    stk.push(a,b);
    cin >> a >> b;
    stk.push(a,b);
    int c;
    double d;
    stk.top(c,d);
    cout << c << " " << d << endl;
    stk.pop();
    stk.top(c,d);
    cout << c << " " << d;
    stk.pop();
    return 0;
}

Obviously, we can find that we cannot implement the top function. We cannot return two elements. If we return, we can only return one structure, or change the practice of top. However, other methods can directly return a pair, because the pair template class is directly pushed into the stack.

Template classes and friends

Just as a class can declare friends, a template class can also declare friend functions. The friends of the template are divided into three categories.

1. Non template friends

2. Constraint template friend

That is, the type of friend depends on the type when the class is instantiated

3. Unconstrained template friends

That is, all materializations of friends are each materialized friend of the class

It doesn't matter if you don't understand, but only if you understand.

1. Non template friends

Declare a regular function as a friend in the template

Because it is not a template function, each instantiation of a template function needs to write a friend function separately

My friend is my friend, and your friend is your friend. Although we are materialized from the template class like brothers, although we are very similar, my friend is not my brother's friend, and my friend is not my brother's friend - the template class says.

template <class T>
class A
{
    friend void func();
    ...
};

Can you do this

template <class T>
class A
{
    friend void func(A &);
    ...
};

Of course not, because there is no such object as A, which can only be done by specific materialization

We can do this

template <class T>
class A
{
    friend void func(A<T>);
    ...
};

Because func is not a class method, but a friend function. It does not belong to a class or a template function, so it should be explicitly materialized when used. During compilation, if the user passes in int type, the compiler will replace T with int. for func, this is an explicit materialization.

Overloaded version

friend void func(A<int>);
friend void func(A<double>);

A small example

#include <iostream>
using namespace std;

template<class T>
class A
{
private:
    T data;
    static int total;
public:
    A(T a) : data(a) { total++; }
    friend void report();
    friend void show(const A<T> &);
};

template<class T>
int A<T>::total = 0;
//This must be explicitly materialized
void show(const A<int> &t)
{
    cout << t.data << endl;
}

//Here too
void show(const A<double> &t)
{
    cout << t.data << endl;
}
void report()
{
    cout << " <int> is " << A<int>::total << endl;
    cout << " <double> is " << A<double>::total << endl;
}
int main()
{
    report();
    A<int> a(1);
    report();
    A<double> b(3.14);
    report();
    A<int> c(10);
    report();
    show(a);
    show(b);
    show(c);
    return 0;
}

Don't forget to initialize static variables!

Operation results

It can also be seen that a < int >, a < double >, are two classes!!! And they're from the same mold.

2. Constraint template friend

Because < int >, < double > are different classes, each materialization of the class should be given a materialization that matches the friend

To put it bluntly, the template function is a friend, which corresponds to the above.

You can modify the previous example so that the friend function itself is called a template

Including the following three supplements

First, define the template of each friend function in front of the class

there T Is a template function T,Is not a template class T,T After instantiation, it is a template class, such as<int>,<string>
template <class T> void show(T &);
template <class T> void report();

Then, declare the function as a friend again in the template class

template <T>
class A
{
    friend void show <> (A<T> &);
    friend void report <T> ();
    ...
};

The < > in the statement indicates the concretization of the template. For show, it can be null because the template type parameters can be inferred from the function parameters

amount to

void show <A<T> (A<T> &);

Finally, you can define a friend function like a template function.

This is exactly the concretization of template function.

It is best to distinguish the Type of friend from the Type of template class

#include <iostream>

using std::cout;
using std::endl;
template <class T> void counts();
template <class T> void report(T &);

template<class TT>
class HasFriend
{
private:
	TT item;
	static int ct;
public:
	HasFriend(const TT& i) : item(i) { ct++; }
	~HasFriend() { ct--; }
	friend void counts <TT>();
	//Why is hasfriend < TT > & not T & t here
	friend void report <>(HasFriend<TT>&);
};

template <class T>
void counts()
{
	cout << "template size: " << sizeof(HasFriend<T>) << ";";
	cout << "template counts(): " << HasFriend<T>::ct << endl;
}

template<class T>
void report(T & t)
{
	cout << t.item << endl;
}

//Note that the initialization should also use the template, which means that it is a static member of the template class
template <class TT>
int HasFriend <TT>::ct= 0;
int main()
{
	counts<int>();
	HasFriend <int> hfi1(10);
	HasFriend <int> hfi2(20);
	HasFriend <double> hfi3(10.5);
	report(hfi1);
	report(hfi2);
	report(hfi3);
	cout << "counts<int>() output:\n";
	counts<int>();
	cout << "counts<double>() output:\n";
	counts<double>();
	return 0;
}

As the program can see, the size of the two counts reports is different, which proves that each type has its own friend function.

It corresponds to the materialization of each count.

3. Unconstrained template friends

It's simple

class A
{
public:
    template<class C, class D> friend void show(C & c, D & d);
};

Outside the class, you only need to remove the semicolon of the declaration, remove friend, and then define it with curly braces.

Template alias

In order to simplify the code, you may use typedef as an alias for the template class, which has a similar function in C++11

template <class T>
  using arrtype = std::array<T,12>;

Array type is the alias of STD:: array < T, 12 >. More powerful than typedef.

//using arrtype = std::array<T,12>;
arrtype<int>; //Equivalent to STD:: array < int, 12 >

arrtype<string>; //Equivalent to STD:: array < string, 12 >

using can also be used for non template classes. This syntax is equivalent to typedef when used for non templates

Finally, some descriptions about materialization and instantiation

Class definitions (instantiations) are generated when a class object is declared and a specific type is specified. For example, the declaration of main will cause the compiler to declare a class and replace T in the template class with the actual type sotr in the declaration

class IC <sotr> sic;

Here, the class name is IC < sotr >, not IC. IC < sotr > is called template materialization

In this case, the compiler will use the generic template to generate an int materialization -- CI < int >, although the object of this class has not been requested

It can provide explicit materialization - override the concrete class declaration defined by the template class. If the template class can't handle a character array well, we can explicitly materialize one

Method starts with template < > (that is, syntax), followed by the template class name, with angle brackets (including the type to be materialized).

template <> class IC(char *)
{
    // T variable_name;
    char * str;
public:
    IC(const char * s) : str(s) {}
    ...
}

In this way, you can obtain a template that specifically deals with character arrays instead of using general template class methods, which is also very similar to the concretization of template functions.

Topics: C++ Design Pattern