Effective C + + reading notes ~7 template and generic programming

Posted by grissom on Tue, 07 Dec 2021 22:04:24 +0100

Article 41: understanding implicit interfaces and compile time polymorphism

Understand implicit interfaces and compile-time polymorphism.

Explicit interface and runtime polymorphism

In object-oriented programming, explicit interface and runtime polymorphism are used to solve problems. For example:

class Widget {
public:
    /* Explicit interfaces consist of function signatures */
    Widget();
    virtual ~Widget();
    virtual std::size_t size() const;
    virtual void normalize();
    void swap(Widget& other);
    ...
};

void doProcessing(Widget& w)
{
    if (w.size() > 10 && w != someNastyWidget) { // size is a virtual function, and the runtime is determined by w dynamic type
        Widget temp(w);
        temp.normalize(); // normalize is a virtual function, and the runtime is determined by w's dynamic type
        temp.swap(w);
    }
}

For the doProcessing parameter w, the so-called explicit interface and runtime polymorphism refer to:

  • Since the W type is declared as a Widget, w must support the Widget interface: the interface declared in the corresponding header file (such as Widget.h) of the Widget class is called explicit interface;
  • Since some member functions of the Widget are virtual, w's calls to those functions will show runtime polymorphism, that is, which function to call will be determined according to w's dynamic type (clause 37) at runtime;

Explicit interfaces are usually composed of function signatures (function name, parameter type and return type), such as the public interface of Widget class: constructor, destructor, function size, normalize, swap and its parameter types, return type, constancy, as well as copy constructor and copy assignment operator synthesized by compiler.

Implicit interface and compile time polymorphism

Different from object-oriented programming, implicit interface and compile time polymorphism are more important in template and generic programming.
Implicit interfaces are not based on function signatures, but consist of valid expressions. The implicit interface completes checking at compile time. See the following example for details,

Rewrite doProcessing to the template version

template<typename T>
void doProcessing(T& w)
{
    if (w.size() > 10 && w != someNastyWidget) {
        T temp(w);
        temp.normalize();
        temp.swap(w);
    }
}

The implicit interface of T (type of w) seems to have these constraints:

  • It must provide a member function called size, which returns an integer value;
  • It must support an operator= Overloaded operator function, used to compare 2 T-type objects.

However, due to the existence of operator overloading, these two constraints do not have to be satisfied. One possibility is that T can support the size member function, and size can also be inherited from base class (not necessarily T's own). Size does not need to return an integer value or a numeric type. The only thing you need is to return an object of type X. The X object + int (type of 10) must be able to call an operator >. In other words, X does not have to support operator >, but can also be of type y. instead, there only needs to be an implicit conversion to convert x objects to y objects, and Y objects support operator >.
Similarly, T does not need to support operator= (call form X.operator!=(Y)), or operator= (x, Y) (overloaded operator! =), where T can be converted to type X and someNastyWidget can be converted to type Y.
In addition, operator & & may also be overloaded instead of logic and operators.

Function size, operator >, operator & &, operator= It is difficult to clearly describe the constraints on the expression, but it is easy to confirm the constraints of the expression as a whole. For example, the condition of an IF statement (if (w.size() > 10 & & W! = somenastywidget)) must be a bool expression.

Summary

1) Both class and template support interface and polymorphism.
2) For class, the interface is explicit and centered on function signature. Polymorphism occurs at runtime through the virtual function (the function to be called through the pointer).
3) For the template parameter, the interface is implicit, based on valid expressions. Polymorphism occurs at compile time through template materialization and function overload resolution.

[======]

Article 42: understand the dual meaning of typename

Understand the two meaning of typename.

When declaring the template parameter, class and typename are identical.

// The following two lines are equivalent
template<class T> class Widget;
template<tyepname T> class Widget;

However, class and typename are not always equivalent. For example,

// Error example: nested dependent names are assumed to be non type unless specified with typename
template<typename C>
void print2nd(const C& container)
{
       if (container.size() >= 2) {
              C::const_iterator iter(container.begin()); // Will not compile. C::const_iterator is a nested dependent name, which is not considered a type by the compiler
              ++iter;
              int value = *iter;
              cout << value;
       }
}

Nested dependent Name: if the name appearing in the template depends on a template parameter, it is called dependent name. If subordinate nouns are nested within a class, such as C::const_iterator, called nested dependent name.
It seems that C::const_iterator is a type, but in fact, the compiler will default that it is a member variable. If you want the compiler to think this is a type, you need to specify it with typename.

if (container.size() >= 2) {
       typename C::const_iterator iter(container.begin()); // OKtypename indicates the nested dependent name C::const_iterator is a type
    ...
}

A general rule for specifying nested dependent type names: when you want to refer to a nested dependent type name in the template, just put the keyword typename in the position immediately before it.
Exception: typename cannot appear before the embedded dependent type name in the base class list, nor can it be repaired as a base class in the member initialization list of the constructor. For example,

template<typename T>
class Derived: public Base<T>::Nested { // The base class inherits the list. typename is not allowed
public:
    explicit Derived(int x) : Base<T>::Nested(x) { // typename is not allowed when constructing member initialization list with
        typename Base<T>::Nested temp; // Nested dependent type name, prefix typename required
        ...
    }
};

typename Another use: use typedef typename Define a short alias for the nested dependent type name.

template<typename IterT>
void workWithIterator(IterT iter)
{
    // The type of what an object of type IterT refers to
    typedef typename std::iterator_traits<IterT>::value_type value_type; // Define alias
    value_type temp(*iter);
    ...
}

Summary

1) When declaring the template parameter, the prefix keyword class and typename are interchangeable;
2) Please use the keyword typename to identify the nested dependent type name; However, it shall not be used as a base class modifier in the base class list or member initialization list;
3) typename related rules have different practices in different compilers;

[======]

Clause 43: learn to handle names in templated base classes

Know how to access names in templatized base classes.

We define the following template based class es for message delivery

class CompanyA {
public:
       void sendClearText(const std::string& msg);
       void sendEncrypted(const std::string& msg);
};

class CompanyB {
public:
       void sendClearText(const std::string& msg);
       void sendEncrypted(const std::string& msg);
};

class MsgInfo { ... }; // Used to save information

// Message sending class
template<typename Company>
class MsgSender {
public:
       void sendClear(const MsgInfo& info) { // Information transfer function
              std::string msg;
              according to info parameter information;
              Company c;
              c.sendCleartext(msg); // OK
       }
       void sendSecret(const MsgInfo& info) {
       }
};

// Message sending class with logging function
template<typename Company>
class LogginMsgSender : public MsgSender<Company> {
public:
       void sendClearMsg(const MsgInfo& info) { // The information transfer function does not have the same name as base msgsender < Company > to avoid masking the non virtual function obtained by inheritance
              Write "before transmission" information to log;
              sendClear(info); // Error: the compiler does not recognize the function
              Write "after transmission" information to log;
       }
};

When the compiler encounters the definition of the derived class template LogginMsgSender, it does not know what class it inherits, because the Company in it is a template parameter. Until LogginMsgSender is materialized, it cannot know exactly what it is. If you can't know what class MsgSender is, you can't know whether it has a sendClear function.

The same is true for exceptional templates

class CompanyZ {
public:
       void sendEncrypted(const std::string& msg);
};

// MsgSender's full specialization for CompanyZ
template<>
class MsgSender<CompanyZ> {
public:
       void sendSecret(const MsgInfo& info) {
       }
};

template<typename Company>
class LogginMsgSender : public MsgSender<Company> {
public:
       void sendClearMsg(const MsgInfo& info) {
              Write "before transmission" information to log;
              sendClear(info); // Error: the compiler does not recognize the function
              Write "after transmission" information to log;
       }
};

The compiler does not recognize the name in the template base class. Solution

1) Add "this - >" before the template base class function call action

template<typename Company>
class LogginMsgSender : public MsgSender<Company> {
public:
       void sendClearMsg(const MsgInfo& info) {
              Write "before transmission" information to log;
              this->sendClear(info); // OK
              Write "after transmission" information to log;
       }
};

2) using declarative
See clause 33. The using declarative can use the "masked" base class name "Bring into a derived class scope. However, it is not that the base class name is masked by the derived class name, but that the compiler does not search in the base class scope because the compiler does not know what MsgSender is. Therefore, we tell it through using to find it in the base class MsgSender.

template<typename Company>
class LogginMsgSender : public MsgSender<Company> {
public:
       using MsgSender<Company>::sendClear; // Tell the compiler to assume that sendClear is inside the base class
       void sendClearMsg(const MsgInfo& info) {
              Write "before transmission" information to log;
              sendClear(info); // OK
              Write "after transmission" information to log;
       }
};

3) Explicitly call functions within base class

template<typename Company>
class LogginMsgSender : public MsgSender<Company> {
public:
       void sendClearMsg(const MsgInfo& info) {
              Write "before transmission" information to log;
              MsgSender<Company>::sendClear(info); // OK
              Write "after transmission" information to log;
       }
};

Disadvantages: if the virtual function is called, the explicit calling method above will turn off the "virtual binding behavior". It is recommended to use 1) or 2).

Summary

1) You can refer to the member name in the base class template through "this - >" in the derived class template, or by a clearly written "base class qualification modifier".

[======]

Clause 44: extract parameter independent code from templates

Factor parameter-independent code out of templates.

template exists to save time and avoid duplicate code.

For example, now you want to write a template for a fixed size square matrix, which supports matrix inversion.

// template is a typical example of code bloating
template<typename T, std::size_t n> // template supports nxn matrix, and the element is an object of type T
class SquareMatrix {
public:
    void invert(); // Finding the inverse of a matrix
};

// client
SquareMatrix<double, 5> sm1; 
sm1.invert();                     // Call squarematrix < double, 5 >:: Convert
SquareMatrix<double, 10> sm2;
sm2.invert();                     // Call squarematrix < double, 10 >:: Convert

The template will present two copies of convert according to the client call, but the rest of the two functions are exactly the same except that the matrix size is different.

Improvement: add a base class to assist in matrix inversion, remove the matrix size from the template parameters and put it into the function parameters of base class -- use the function parameters to eliminate the non type template parameters

// All matrices of a given element object type share the same SquareMatrixBase
template<typename T> // Size independent base class for square matrix
class SquareMatrixBase { 
protected: // Why use protected instead of public/private? Because squarematrixbase:: convert is just a way to "avoid duplication of derived class code"
    void invert(std::size_t matrixSize); // Find the inverse matrix with a given size
};

tempalte<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> { // Why is private inheritance? It is not an is-a relationship, but a has-a relationship, which assists in matrix inversion
private:
    using SquareMatrixBase<T>::invert;    // Avoid masking the invert of the base
public:
    void invert() { this->invert(n); }
};

The convert with parameters is moved to base class SquareMatrixBase, so that only the type of base class matrix element object is parameterized, not the matrix size. Therefore, for a given element object type, all matrices share the same SquareMatrixBase class. This can effectively reduce the amount of code.

Why is the function squarematrixbase:: convert protected instead of public or private?
Because this function is only a method to "avoid duplication of derived class code", the customer does not need to know and needs to be called by derived class, so it should be protected.

Why use squarematrixbase:: Convert in the SquareMatrix class?
According to method 2 of Clause 43, using the using declarative is to tell the compiler to assume that the convert is within the base class.

Why does the squarematrix:: convert function still use the "this - >" token?
Because derived class also implements the function convert with the same name, if not, the function name in the templated base class will be masked by derived class according to Clause 43.

Why does SquareMatrix inherit SquareMatrixBase from private instead of public?
Because SquareMatrix is not SquareMatrixBase, and the two are not is-a relationship, SquareMatrixBase exists only to help SquareMatrix find the inverse matrix.

The next question is,
How to store matrix data? Should it be stored in SquareMatrix or SquareMatrixBase?
A matrix can be stored in a dimension group T data[n*n]. The most clear thing about how much data to store should be SquareMatrix. If SquareMatrixBase wants to operate the matrix, it can point to the array with a pointer.

// Example of using static one-dimensional array to store matrix content

template<typename T>
class SquareMatrixBase {
protected:
    SquareMatrixBase(std::size_t n, T* pMem) : size(n), pData(pMem) {}
    void setDataPtr(T* ptr) { pData = ptr; }
    ...
private:
    std::size_t size; // Matrix size
    T* pData; // Point to matrix content
};

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
    SquareMatrix() : SquareMatrixBase<T>(n, data) { } // Initialize the list with the constructor and pass the matrix size and data pointer to the base class
    ...
private:
    T data[n * n]; // Store matrix content
};

You can also dynamically allocate memory, put the data of the matrix into the heap, and then give it to the base class.

The above method of SquareMatrix is to eliminate template non type parameters (matrix size) through function parameters.
Some may be caused by type parameters. For example, some platforms have the same binary representation of int and long, so the member functions of vector and vector may be exactly the same. Some linker s In this case, you can use a unique underlying implementation for each member function to implement some member functions to operate on strongly typed pointers (T), so that they can call another function that operates on untyped pointer s (void), and the latter can complete the actual work.

Summary

1) Template generates multiple class es and functions, so no template code should be dependent on a template parameter that causes expansion;
2) Code inflation caused by non type template paramter can often be eliminated by replacing template parameter with function parameter or class member variable;
3) The code expansion caused by type parmaeter can often be reduced. The method is to let the instance type with exactly the same binary representation share the implementation code;

[======]

Clause 45: accept all compatible types using member function templates

Use member function template to accept "all compatible types."

Real pointer implicit conversion

smart pointer is an object that "acts like a pointer" and provides functions that the pointer does not have.
For example, how can shared_ptr and unqiue_ptr mentioned in Clause 13 be used to automatically delete heap based resources at the right time.

One thing that real pointers do well is to support implicit conversion, which is reflected in two aspects:
1) derived class pointer can be implicitly converted to base class pointer;
2) A pointer to a non const object can be converted to a const object without explicit cast;
For example,

class Top { ... };
class Middle: public Top { ... };
class Bottom: public Middle { ... };
Top* pt1 = new Middle; // Convert Middle * to top * (Middle * = > Top *)
Top* pt2 = new Bottom; // Bottom* => Top*
const Top* pt2 = pt1;  // const Top* => Top*

Smart pointer implicit conversion

But if you want to simulate the above conversion with a smart pointer, how do you do it?
For example, to perform the following conversion:

template<typename T>
class SmartPtr {
public:
    explicit SmartPtr(T* realPtr);
    ...
};

// The smart pointer conversion that the client wants to perform
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle); // SmartPtr<Middle> => SmartPtr<Top>
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom); // SmartPtr<Bottom> => SmartPtr<Top>
SmartPtr<const Top> pt3 = pt1; // SmartPtr<Top> => SmartPtr<const Top>

The two existing smartptrs in template have nothing to do with SmartPtr. If you want to obtain the conversion ability between SmartPtr class es, you must write them clearly.

Template and Generic Programming

In the above example, when the client performs smart pointer conversion, it actually creates a new smart pointer object. We can focus on how to write the constructor of smart pointer to meet our transformation needs.

member template

So how do I write a constructor for template class?
If we write constructors for SmartPtr and SmartPtr, we may add another template class that inherits from SmartPtr or SmartPtr one day. We may need to add another constructor or even modify the constructor of the original base class.
In fact, there is no limit to the number of constructors required, because a template can be wirelessly embodied to generate an infinite number of functions. Therefore, instead of writing a constructor for SmartPtr, we need to write a construction template for it. Such a template is the so-called member function template (member template for short), which is used to generate functions for class:

// Construct a template. For any type of T and u, you can generate a smartptr < T > object according to the smartptr < U > object
template<tyepname T>
class SmartPtr {
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other); // member template to generate the copy constructor
    ...
};

// Client call: smartptr < U > = > smartptr < T >, where T and u are of any type
SmartPtr<U> pu = SmartPtr<U>(new XXX);
SmartPtr<T> pt = pu;

This kind of template class object of type T is used to construct the constructor of template class of type U, which is called "generalized copy constructor".

Why is the generalized copy constructor above not declared explicit?
This is intentional, because the conversion between the original pointer types is implicit, and there is no need to write the transition action (cast), so it is reasonable for the smart pointer to follow this behavior. Omitting explicit in the templated constructor is for this purpose.

How to write "return copy constructor"?

How to write "return copy constructor" after declaration? We want to create a SmartPtr based on a SmartPtr, but we don't want to create a SmartPtr based on a SmartPtr, because the two are contradictory to inheritance (Article 32). At the same time, we do not want to create a SmartPtr based on a SmartPtr, because there is no corresponding implicit conversion behavior to convert "int * to double *" in reality. In other words, we must pick or screen out the member functions created by the member template.

Suppose SmartPtr follows unique_ptr and shared_ The example provided by PTR also provides a get member function to return a copy of the original pointer held by the smart pointer object (clause 15). Then we can constrain the conversion behavior in the "construction template" implementation code to make it meet our expectations:

template<typename T>
class SmartPtr {
public:
    // Construct template, i.e. generalize copy constructor
    template<typename U>
    SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) { ... } // Initialize this - > heldPtr with other's heldPtr
    
    T* get() const { return heldPtr; } // Returns a copy of the original pointer
    ...
private:
    T* heldPtr; // Built in pointer held by SmartPtr (original pointer)
};

Our construction template uses the member initialization list to initialize the member variable of type T in SmartPtr, and takes the pointer of type U (held by SmartPtr < U >) as the initial value. Precondition: there is implicit conversion, which can convert u pointer to t pointer. That is, the constructor is compiled only when it gets a type compatible with its arguments.

The member function template is not limited to constructors and is often used for assignment operations. Such as shared_ptr,unique_ptr,weak_ The construction behavior of PTR and the division of weak_ Assignment operation outside PTR (why?).

For example, shared in TR1 specification_ An excerpt from PTR,

template<class T>
class shared_ptr {
public:
    template<class Y>
    explicit shared_ptr(Y* p); // Construct built-in pointers from compatible

    template<class Y>
    shared_ptr(shared_ptr<Y> const& r); // Construct from compatible shared_ptr. Generalized copy constructor

    template<class Y>
    explicit shared_ptr(weak_ptr<Y> const& r); // Construct from compatible weak_ptr

    template<class Y>
    explicit shared_ptr(unique_ptr<Y>& r); // Construct from compatible unique_ptr

    template<class Y>
    shared_ptr& operator=(shread_ptr<Y> const& r); // assignment operator, from compatible shread_ptr

    template<class Y>
    shared_ptr& operator=(unique_ptr<Y>& r); // assignment operator, from compatible unique_ptr
    ...
};

All the above constructors are explicit, except for the "generalized copy constructor". Why?
Because it means a shared_ The PTR type is implicitly converted to another shared_ptr types are allowed, but implicit conversion from built-in pointers or other smart pointer types is not recognized. However, if it is a display transformation, such as cast forced transformation, it is OK.

Why is unique passed to constructor and operator =_ PTR parameters are not const?
Because Clause 13 refers to copying unique_ptr will lead to the original unique_ptr changes (points to null). This is by unique_ The characteristics of PTR determine that only one unique is allowed at the same time_ PTR points to a given object. When the original unique_ The object pointed to by PTR is handed over to the new unique_ When PTR points to, the original unique_ptr points to null.

Declare generalized copy constructors and copy constructors

The member template does not change the language rules. Clause 5 mentioned that the compiler may generate four member functions for us: default constructor, copy constructor, copy assignment operator and destructor. (Note: C++11 adds a new move constructor)

If the program needs a copy constructor and you don't declare it, the compiler will secretly generate one for you. Declaring a generalized copy constructor (which is a member template) in class does not prevent compilers from generating their own copy constructor (a non template), so if you want to control all aspects of copy construction, you must declare both a generalized copy constructor and a "normal" copy constructor. The same rule applies to assignment operators.
Here is shared_ A definition summary of PTR:

tempalte<class T>
class shared_ptr {
public:
    shared_ptr(shared_ptr const& r); // copy constructor

    template<class Y>
    shared_ptr(shared_ptr<Y> const& r); // Generalized copy constructor

    shared_ptr& opeartor=(shared_ptr const& r); // copy assignment
    
    template<class Y>
    shared_ptr& opeartor=(shared_ptr<Y> const& r);  // Generalized copy assignment
    ...
};

Summary

1) Please use the member function template to generate functions that "accept all compatible types";
2) If you declare that the member template is used for "generalization copy construction" or "generalization assignment operation", you still need to declare the normal copy constructor and copy assignment operator.

[======]

Clause 46: when type conversion is required, please define non member functions for the template

Define non-member functions inside template when type conversions are desired.

This clause is similar to Clause 24. Clause 24 discusses why only non member functions can "implement implicit type conversion on all arguments". This clause also takes the operator * of Rational class as an example.

// Rational template version of Clause 24
template<typename T>
class Rational {
public:
    Rational(const T& numerator = 0, const T& denominator = 1); // Parameters are passed by reference to avoid copying, and modification can affect the actual parameters
    const T numerator() const; // molecule
    const T denominator() const; // denominator
    ...
};

// The non member function overloads the operator*
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs)
{ ... }

But unlike the example of Clause 24,

Rational oneHalf(1,2);
Rational result = oneHalf * 2; // MSVC reports an error: there is no "*" operator matching these operands

It is also overloaded operator *. Why does the example here report an error?
In Clause 24, the compiler knows what function we are trying to call (the operator that accepts two Rational parameters), but here the compiler does not know which function we want to call, because they want to try to figure out what function is embodied (generated) by the template called operator. To materialize a function named "operator and accept two Rational parameters", you must first calculate what T is. The problem is that the compiler can'T do it.
To push out T, first look at the argument types of the operator call action: Rational (type of oneHalf) and int (type of number 2). The two parameters are considered separately:
1) For derivation with oneHalf (Rational), the first parameter of operator * is declared as const Rational, and the type passed is Rational, so T is int.
2) The derivation is carried out with the number 2 (int). Since the implicit type conversion function is never considered in the derivation process of the template argument (and the implicit type conversion through the constructor is not considered), the compiler cannot convert 2 to Rational using Rational's non explicit constructor. However, such implicit conversion is used in the process of function call, on the premise that before a function can be called, you must first know the existence of that function. In order to know it, the parameter type must be derived for the relevant function template before the appropriate function can be embodied.

There is an easy way:
The friend declaration in the template class can refer to a specific function. This means that class Rational can declare operator as one of its friend functions. class template does not rely on template argument derivation (argument derivation is only applied to function template), so the compiler can always know the T type when class Rational is embodied. If Rational class declares the appropriate operator as its friend function, the whole problem can be simplified:

template<typename T>
class Rational {
public:
    friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);
    // Because within the class template, the template name (Rational) can be used as a short expression for "template and its parameters" (rational < T >)
    // Therefore, the above declaration is equivalent to the following declaration < = >
    friend const Rational operator*(const Rational& lhs, const Rational& rhs) { // Use abbreviated expressions
        return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
    }
};

Note: if the operator implementation is placed outside the class template (such as. cpp file), it can be compiled, but cannot be linked.
Reason why it can be compiled: because the compiler knows which function we want to call through the template function operator.
Cannot link reason: operator* template cannot be defined outside class template, but must be defined inside class.

Of course, we can call an external function doMultiply through the operator* template to simplify the operator *.

template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs) 
{
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

template<typename T>
class Rational {
public:
    friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);
    // Because within the class template, the template name (Rational) can be used as a short expression for "template and its parameters" (rational < T >)
    // Therefore, the above declaration is equivalent to the following declaration < = >
    friend const Rational operator*(const Rational& lhs, const Rational& rhs) { // Use abbreviated expressions
        return doMultiply(lhs, rhs);
    }
};

Summary

1) When we write a class template, please define the functions related to the template and used for implicit type conversion as "friend functions inside the class template".
Two aspects are implied: the friend function of class template and its internal implementation.

[======]

Clause 47: please use traits classes to represent type information

Use traits clases for information about types.

In STL, containers and algorithms are separated and linked by iterators. How does the algorithm extract the type of container element from the iterator class?
This requires the traits class technology.

Before using traits class technology, let's look at how to obtain element types:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d); // Function template: move iterator iter to d position

// Question: how do I get the element type that IterT refers to?
// If IterT supports random access (+ =) operation, advance should do iter += d;
// If it is not supported, what advance needs to do is to repeatedly implement + + iter (or -- iter) d times.

There are five common iterators in C + +: input, output, forward, bidirectional, and random access.
C++ STL provides an exclusive tag struct ure for confirmation:

struct input_iterator_tag
       {      // identifying tag for input iterators
       };
struct output_iterator_tag
       {      // identifying tag for output iterators
       };
struct forward_iterator_tag
       : input_iterator_tag
       {      // identifying tag for forward iterators
       };
struct bidirectional_iterator_tag
       : forward_iterator_tag
       {      // identifying tag for bidirectional iterators
       };
struct random_access_iterator_tag
       : bidirectional_iterator_tag
       {      // identifying tag for random-access iterators
       };

For details of class 5 iterators, see 5th page 366 of C++Primer Chinese version, or https://blog.csdn.net/CSDN_564174144/article/details/76231626

How does the iterator get the element type referred to by IterT and implement advance?
One option is:

template <typename IterT, typename DistT>
void advance(Iter& iter, DistT d)
{
    if (iter is a random access iterator) { // Only random access iterator supports random access operations
        iter += d; // The random access iterator uses iterators to perform arithmetic operations
    }
    else {
        if (d >= 0) { while(d--) ++iter; }
        else { while (d++) --iter; }
    }
}

In this way, we must first judge whether ITER is a random access iterator, that is, whether ITER type (ITER &) is a random access iterator. To obtain the type represented by ITER, we can obtain it at compile time through traits class technology.

traits class technology

traits class technology is not a C + + keyword, but a common protocol followed by C + + programmers. One of its requirements is that the performance of built-in types and user-defined types must be as good as each other.
Specifically, the type's traits information is put into a template and its specialized version. When different container templates are instantiated, the type information carries different traits information in different specialized versions.

Example of extracting element type information using iterators:

// General version
template<class IterT>
struct my_iterator_traits {
       typedef typename IterT::value_type value_type; // traits information
};
// Partial specialization version
template<class IterT>
struct my_iterator_traits<IterT*> {
       typedef IterT value_type; // traits information
};
void fun(int a) {
       cout << "func(int) is called" << endl;
}
void fun(double a) {
       cout << "func(double) is called" << endl;
}
void fun(char a) {
       cout << "func(char) is called" << endl;
}

// client
int main()
{
       my_iterator_traits<vector<int>::iterator>::value_type a = 0;
       fun(a); // Print "func(int) is called"
       my_iterator_traits<vector<double>::iterator>::value_type b = 1;
       fun(b); // Print "func(double) is called"
       my_iterator_traits<vector<char>::iterator>::value_type c = 2;
       fun(c); // Print "func(char) is called"
       return 0;
}

For a brief introduction to traits class technology, see
https://blog.csdn.net/lihao21/article/details/55043881

Summary

1) traits class makes "type related information" available at compile time, using template and template specialization.

[======]

Clause 48: understanding template metaprogramming

Be aware of template metaprogramming.

The specific content of meta programming is omitted temporarily.

Summary

1) Template metaprogramming (TMP) can move the work from run-time to compile time, so as to realize early error detection and higher execution efficiency;
2) TMP can be used to generate customized code based on combinations of policy choices, and can also be used to avoid generating code that is not suitable for some special types.

[======]

Topics: C++