"C++ Advanced Programming" reading notes (twelve: using templates to write generic code)

1. References

2. It is recommended to read the book "21 Days to Learn C++" to get started. The link of the notes is as follows

1. Template overview

  • Templates take the concept of parameterization a step further, allowing parameterization not only of values, but also of types. Types in C++ include not only primitive types, such as int and double, but also user-defined classes, such as SpreadsheetCell and CherryTree. Using templates, not only can you write code that doesn't depend on specific values, but you can also write code that doesn't depend on the types of those values

2. Class Templates

  • A class template defines a class in which the types of some variables, the return type of a method, and/or the parameter types of a method are specified as parameters. Class templates are mainly used for containers, or data structures used to hold objects

2.1 Writing class templates

  • Suppose you want a generic board class that you can use as a chess board, checkers board, tic-tac-toe board , or any other two-dimensional board. For this board to be versatile, the board should be able to hold chess pieces, checkers pieces, tic tac toe pieces, or any other type of game
2.1.1 Grid class definition
  • The first line indicates that the class definition below is based on a type of template. Just like in a function, the parameter name is used to indicate the parameter to be passed in by the caller, and the template parameter name (such as T) is used in the template to indicate the type to be specified by the caller
  • When specifying template type parameters, the keyword class can be used instead of typename, but class can cause some misunderstandings, because the word implies that the type must be a class , while the actual type can be class, struct, union, primitive types such as int or double etc.
    template <typename T>
    class Grid {
          
          
        // ...
    };
    
2.1.2 Method definition of Grid class
  • The template <typename T> access specifier must precede every method definition in the Grid template
  • The template requires that the implementation of the method is also placed in the header file, because the compiler needs to know the complete definition, including the definition of the method, before creating an instance of the template
    template <typename T>
    Grid<T>::Grid(size_t width, size_t height) : mWidth(width), mHeight(height) {
          
           // 构造函数
        // ...
    }
    
2.1.3 Using the Grid template
  • When creating a grid object, you cannot use Grid alone as a type, and you must specify the element type that this grid saves . The process of creating a template class object for a certain type is called template instantiation. Here's an example

    Grid<int> myIntGrid;
    Grid<double> myDoubleGrid(11, 11);
    
    myIntGrid.at(0, 0) = 10;
    // at() 方法返回 std:optional 引用。optional 可包含值,也可不包含值
    // 如果 optional 包含值,value_or() 方法返回这个值;否则返回给 value_or() 提供的实参
    int x = myIntGrid.at(0, 0).value_or(0);
    
    Grid<int> grid2(myIntGrid); // 复制构造函数
    Grid<int> anotherIntGrid;
    anotherIntGrid = grid2; // 赋值运算符
    
  • If you want to declare a function or method that receives a Grid object, you must specify the type of element stored in the grid in the Grid type

    void processIntGrid(Grid<int>& grid) {
          
          
        // ...
    }
    
  • To avoid having to write the full Grid type name each time, eg Grid<int>, a simpler name can be specified via a type alias

    using IntGrid = Grid<int>;
    
  • The data type that Grid template can save is not only int. For example, you can instantiate a grid that holds a SpreadsheetCell

    Grid<SpreadsheetCell> mySpreadsheet;
    SpreadsheetCell myCell(1.234);
    mySpreadsheet.at(3, 4) = myCell;
    
  • Grid templates can also hold pointer types

    Grid<const char*> myStringGrid;
    myStringGrid.at(2, 2) = "hello";
    
  • The type specified by the Grid template can even be another template type

    Grid<vector<int>> gridOfVectors;
    vector<int> myVector{
          
          1, 2, 3, 4};
    gridOfVectors.at(5, 6) = myVector;
    
  • The Grid template can also dynamically allocate Grid template instances on the heap

    auto myGridOnHeap = make_unique<Grid<int>>(2, 2);
    myGridOnHeap->at(0, 0) = 10;
    int x = myGridOnHeap->at(0, 0).value_or(0);
    

2.2 Principles of Compiler Processing Templates

  • When the compiler encounters a template method definition, it performs syntax checking but does not compile the template. The compiler cannot compile the template definition because it does not know what type to use
  • When the compiler encounters an instantiated template, such as Grid<int> myIntGrid, it replaces every T in the template class definition with an int, thus generating the code for the int version of the Grid template. When the compiler encounters another instance of this template, it generates another version of the Grid class

2.3 Spread template code across multiple files

  • Typically, you put class definitions in a header file and method definitions in a source code file. The code that creates or uses a class object will include the corresponding header file through #include, and access these method codes through the linker
  • Templates don't work this way . Since the compiler needs to use these "templates" to generate the actual method code for the instantiated type, in any source code file that uses templates, the compiler should have access to both the template class definition and the method definition . There are several mechanisms to satisfy this containment requirement
2.3.1 Put the template definition in the header file
  • Method definitions can be placed directly in the same header file as the class definition. When a source file that uses this template includes this file via #include, the compiler has access to all the code it needs. Alternatively, you can put the template method definition in another header file, and then include this header file via #include in the header file of the class definition
    // Grid.h
    template <typename T>
    class Grid {
          
          
        // ...
    };
    // 一定要保证方法定义的 #include 在类定义之后,否则代码无法编译
    #include "GridDefinition.h"
    
2.3.2 Put the template definition in the source file
  • Method definitions can be placed in a source code file. However, the code that uses the template still needs to have access to the definition, so the source file for the class method implementation can be included in the template class definition header file by #include
    // Grid.h
    template <typename T>
    class Grid {
          
          
        // ...
    };
    
    #include "Grid.cpp"
    

2.4 Template parameters

2.4.1 Non-type template parameters
  • Non-type template parameters can only be integer types (char, int, long, etc.), enumeration types, pointers, references, and std::nullptrt. Starting from C++17, you can also specify auto, auto&, and auto* as the type of non-type template parameters. At this time, the compiler will automatically deduce the type
    template <typename T, size_t WIDTH, size_t HEIGHT>
    class Grid {
          
          
        // ...
    };
    // 实例化模板
    Grid<int, 10, 10> myGrid;
    Grid<int, 10, 10> anotherGrid;
    myGrid.at(2, 3) = 42;
    anotherGrid = myGrid;
    cout << anotherGrid.at(2, 3).value_or(0);
    
2.4.2 Default values ​​for type parameters
  • If you continue to use height and width as template parameters, you may need to provide default values ​​for height and width (which are non-type template parameters)
    template <typename T = int, size_t WIDTH = 10, size_t HEIGHT = 10>
    class Grid {
          
          
        // ...
    };
    
    // 不需要在方法定义的模板规范中指定 T、WIDTH 和 HEIGHT 的默认值
    template <typename T, size_t WIDTH, size_t HEIGHT>
    const std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(size_t x, size_t y) const {
          
          
        // ...
    }
    
    // 实例化 Grid 时,可不指定模板参数,只指定元素类型,或者指定元素类型和宽度,或者指定元素类型、宽度和高度
    Grid<> myGrid;
    Grid<int> myGrid2;
    Grid<int, 5> myGrid3;
    Grid<int, 5, 5> myGrid4;
    
2.4.3 Template parameter deduction for constructors
  • C++17 added features to support automatic deduction of template arguments from arguments passed to class template constructors . Prior to C++17, all template parameters had to be specified explicitly for class templates
  • For example, the standard library has a class template std:pair (defined in <utility>). pair stores two values ​​of two different types, which must be specified as template parameters
    std::pair<int, double> pair1(1, 2.3);
    
  • To avoid the need to write template parameters, a helper function template std:make_pair() is available. Function templates have always supported automatic deduction of template arguments based on the arguments passed to the function template. Therefore, make_pair() can automatically deduce the template type parameter from the value passed to it
    auto pair2 = std::make_pair(1, 2.3);
    
  • In C++17, such auxiliary function templates are no longer needed, and the compiler can automatically deduce template type parameters based on the actual parameters passed to the constructor
    std::pair pair3(1, 2.3);
    

    The premise of the deduction is that all template parameters of the class template either have default values, or are used as parameters in the constructor.
    std::unique_ptr and shared_ptr will disable type deduction and need to continue to use make_unique() and make_shared() to create

2.5 Method templates

  • C++ allows individual methods in templated classes, either in class templates or in non-templated classes

    Cannot write virtual methods and destructors using method templates

  • An object of type Grid<int> cannot be assigned to an object of type Grid<double>, nor can a Grid<double> be constructed from a Grid<int>

  • Both the Grid copy constructor and operator= take a reference to a const <Grid> as a parameter

    Grid(const Grid<T>& src);
    Grid<T>& operator=(const Grid<T>& rhs);
    
  • When instantiating Grid<double> and trying to call the copy constructor and operator=, the compiler generates methods from these prototypes

    Grid(const Grid<double>& src);
    Grid<double>& operator=(const Grid<double>& rhs);
    
  • In the generated Grid<double> class, neither the constructor nor operator= accepts Grid<int> as a parameter, but it can be solved by double template : adding templated copy constructor and assignment operator to the Grid class can generate Methods for Converting from One Mesh Type to Another

    template <typename T>
    class Grid {
          
          
    public:
        // ...
        template <typename E>
        Grid(const Grid<E>& src);
    
        template <typename E>
        Grid<T>& operator=(const Grid<E>& rhs);
    };
    
    • The following is the definition of the new copy constructor. The line declaring the class template (with the T parameter) must be placed before the line declaring the member template (with the E parameter)
    template <typename T>
    template <typename E>
    Grid<T>::Grid(const Grid<E>& src) : Grid(src.getWidth(), src.getHeight()) {
          
          
        // ...
    }
    

2.6 Specialization of class templates

  • Another implementation of templates is called template specialization, through which a special implementation can be written for a template when the template type is replaced by a specific type
  • When writing a template class specialization, you must indicate that this is a template and the specific type for which the template is being written. Here is the specialization for const char*
    // 下述语法告诉编译器,这个类是 Grid 类的 const char* 特例化版本
    template <>
    class Grid<const char*> {
          
          
        // ...
    };
    
    Grid<int> myIntGrid;
    Grid<const char*> stringGrid(2, 2);
    

    Note that in this specialization, do not specify any type variables, such as T, but directly handle const char*

  • The main benefit of specialization is that it can be hidden from the user : when the user creates a Grid of type int or SpreadsheetCell, the compiler generates code from the original Grid template; when the user creates a Grid of type const char*, the compiler uses a special case of const char* Optimized version, all of these are done automatically in the background
  • When you specialize a template, you do not inherit any code: specialization, unlike derivation, must rewrite the entire implementation of the class, and is not required to provide methods with the same name or behavior
    • For example, Grid's const char* special case only implements the at() method, returning std::optional<std::string> instead of std::optional<const char*>

2.7 Deriving from a class template

  • Derivable from a class template. If a derived class inherits from the template itself, then the derived class must also be a template . Also, a specific instance can be derived from a class template, in which case the derived class need not be a template
  • Let's say the generic Grid class doesn't provide enough board functionality. Specifically, the move() method is added to the board, allowing pieces on the board to be moved from one position to another. Below is the class definition for this GameBoard template
    • This GameBoard template is derived from the Grid template, so it inherits all the functions of the Grid template
    • The syntax of inheritance is the same as that of ordinary inheritance, the difference is that the base class is Grid<T> instead of Grid
    #include "Grid.h"
    
    template <typename T>
    class GameBoard : public Grid<T> {
          
          
    public:
        explicit GameBoard(size_t width = Grid<T>::kDefaultWidth,
                           size_t height = Grid<T>::kDefaultHeight);
        void move(size_t xSrc, size_t ySrc, size_t xDest, size_t yDest);
    };
    

2.8 Inheritance or specialization

  • Extend the implementation and use polymorphism through inheritance, and customize the implementation of specific types through specialization
    insert image description here

3. Function templates

  • Templates can also be written for individual functions. For example, you can write a generic function that looks up a value in an array and returns the index of that value
    // size_t 是一个无符号整数类型
    // 通过这样的转换,可以将负值转换为等效的正值,以便在使用无符号整数时表示特殊的未找到或无效状态
    static const size_t NOT_FOUND = static_cast<size_t>(-1);
    
    template <typename T>
    // 这个 Find() 函数可用于任何类型的数组
    size_t Find(const T& value, const T* arr, size_t size) {
          
          
        for (size_t i = 0; i < size; ++i) {
          
          
            if (arr[i] == value) {
          
          
                return i;
            }
        }
        return NOT_FOUND;
    }
    
  • Like class template method definitions, function template definitions (not just prototypes) must be available in all source files where they are used. Therefore, if multiple source files use a function template, or use explicit instantiation as discussed earlier, place its definition in the header file
  • Template parameters of function templates can have default values, just like class templates

3.1 Specialization of function templates

template<>
size_t Find<const char*>(const char* const& value, const char* const* arr, size_t size) {
    
    
    for (size_t i = 0; i < size; ++i) {
    
    
        if (strcmp(arr[i], value) == 0) {
    
    
            return i;
        }
    }
    return NOT_FOUND;
}
const char* word = "two";
const char* words[] = {
    
    "one", "two", "three", "four"};
const size_t sizeWords = std::size(words);
size_t res;

res = Find<const char*>(word, words, sizeWords);
res = Find(word, words, sizeWords);

3.2 More introduction to template argument deduction

  • The compiler deduces the type of the template parameter according to the actual parameter passed to the function template, and for the template parameter that cannot be deduced, it needs to be explicitly specified . For example, the following add() function template requires three template parameters: the type of the return value and the types of the two operands
    template <typename RetType, typename T1, typename T2>
    RetType add(const T1& t1, const T2& t2) {
          
          
        return t1 + t2;
    }
    
  • When calling this function template, you can specify all three parameters as follows
    auto result = add<long long, int, int>(1, 2);
    
  • But because the template parameters T1 and T2 are parameters of the function, the compiler can deduce these two parameters, so when calling add(), you can only specify the type of the return value
    auto result = add<long long>(1, 2);
    
  • You can also provide a default value for the return type template parameter, so that you can call add() without specifying any type
    template <typename RetType = long long, typename T1, typename T2>
    RetType add(const T1& t1, const T2& t2) {
          
          
        return t1 + t2;
    }
        
    auto result = add(1, 2);
    

3.3 Return types of function templates

  • Wouldn't it be better to let the compiler deduce the type of the return value? It is indeed good, but the return type depends on the template type parameter, starting from C++14, you can ask the compiler to automatically deduce the return type of the function
    template <typename T1, typename T2>
    auto add(const T1& t1, const T2& t2) {
          
          
        return t1 + t2;
    }
    
  • However, when using auto to deduce the expression type, the reference and const qualifiers are removed. After C++14, the add() function can be written using decltype(auto) to avoid removing any const and reference qualifiers
    template <typename T1, typename T2>
    decltype(auto) add(const T1& t1, const T2& t2) {
          
          
        return t1 + t2;
    }
    

4. Variable Templates

  • In addition to class templates, class method templates, and function templates , C++14 adds the ability to write variable templates
    template <typename T>
    constexpr T pi = T(3.14159265);
    
  • The above is a variable template for the pi value. In order to get the value of pi in a certain type, the following syntax can be used
    float piFloat = pi<float>;
    long double piLongDouble = pi<long double>;
    
  • This will always result in an approximation of pi that is representable in the requested type. Like other types of templates, variadic templates can also be specialized

Guess you like

Origin blog.csdn.net/qq_42994487/article/details/131433144