Notas de lectura de "Programación avanzada en C++" (doce: uso de plantillas para escribir código genérico)

1. Referencias

2. Se recomienda leer el libro "21 Días para aprender C++" para empezar, el enlace de las notas es el siguiente

1. Descripción general de la plantilla

  • Las plantillas llevan el concepto de parametrización un paso más allá, permitiendo la parametrización no solo de valores, sino también de tipos. Los tipos en C++ incluyen no solo tipos primitivos, como int y double, sino también clases definidas por el usuario, como SpreadsheetCell y CherryTree. Al usar plantillas, no solo puede escribir código que no depende de valores específicos, sino que también puede escribir código que no depende de los tipos de esos valores.

2. Plantillas de clase

  • Una plantilla de clase define una clase en la que los tipos de algunas variables, el tipo de retorno de un método y/o los tipos de parámetros de un método se especifican como parámetros. Las plantillas de clase se utilizan principalmente para contenedores o estructuras de datos que se utilizan para contener objetos.

2.1 Plantillas de clases de escritura

  • Suponga que desea una clase de tablero genérica que pueda usar como tablero de ajedrez, tablero de damas, tablero de tres en raya o cualquier otro tablero bidimensional. Para que este tablero sea versátil, debe poder contener piezas de ajedrez, damas, tres en raya o cualquier otro tipo de juego.
2.1.1 Definición de clase de cuadrícula
  • La primera línea indica que la definición de clase a continuación se basa en un tipo de plantilla. Al igual que en una función, el nombre del parámetro se usa para indicar el parámetro que debe pasar la persona que llama, y ​​el nombre del parámetro de la plantilla (como T) se usa en la plantilla para indicar el tipo que debe especificar la persona que llama.
  • Al especificar parámetros de tipo de plantilla, la palabra clave class se puede usar en lugar de typename, pero class puede causar algunos malentendidos, porque la palabra implica que el tipo debe ser una clase , mientras que el tipo real puede ser class, struct, union, tipos primitivos como como int o doble, etc.
    template <typename T>
    class Grid {
          
          
        // ...
    };
    
2.1.2 Definición del método de la clase Grid
  • El especificador de acceso de la plantilla <typename T> debe preceder a cada definición de método en la plantilla de Grid
  • La plantilla requiere que la implementación del método también se coloque en el archivo de encabezado, porque el compilador necesita conocer la definición completa, incluida la definición del método, antes de crear una instancia de la plantilla.
    template <typename T>
    Grid<T>::Grid(size_t width, size_t height) : mWidth(width), mHeight(height) {
          
           // 构造函数
        // ...
    }
    
2.1.3 Uso de la plantilla de cuadrícula
  • Al crear un objeto de cuadrícula, no puede usar Grid solo como tipo y debe especificar el tipo de elemento que guarda esta cuadrícula . El proceso de creación de un objeto de clase de plantilla para un determinado tipo se denomina creación de instancias de plantilla. aquí hay un ejemplo

    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; // 赋值运算符
    
  • Si desea declarar una función o método que recibe un objeto Grid, debe especificar el tipo de elemento almacenado en la cuadrícula en el tipo de cuadrícula

    void processIntGrid(Grid<int>& grid) {
          
          
        // ...
    }
    
  • Para evitar tener que escribir el nombre completo del tipo de cuadrícula cada vez, por ejemplo, Grid<int>, se puede especificar un nombre más simple a través de un alias de tipo

    using IntGrid = Grid<int>;
    
  • El tipo de datos que la plantilla de Grid puede guardar no solo es int. Por ejemplo, puede crear una instancia de una cuadrícula que contenga una SpreadsheetCell

    Grid<SpreadsheetCell> mySpreadsheet;
    SpreadsheetCell myCell(1.234);
    mySpreadsheet.at(3, 4) = myCell;
    
  • Las plantillas de cuadrícula también pueden contener tipos de puntero

    Grid<const char*> myStringGrid;
    myStringGrid.at(2, 2) = "hello";
    
  • El tipo especificado por la plantilla de cuadrícula puede incluso ser otro tipo de plantilla

    Grid<vector<int>> gridOfVectors;
    vector<int> myVector{
          
          1, 2, 3, 4};
    gridOfVectors.at(5, 6) = myVector;
    
  • La plantilla de Grid también puede asignar dinámicamente instancias de plantilla de Grid en el montón

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

2.2 Principios de las plantillas de procesamiento del compilador

  • Cuando el compilador encuentra una definición de método de plantilla, realiza una verificación de sintaxis pero no compila la plantilla. El compilador no puede compilar la definición de la plantilla porque no sabe qué tipo usar
  • Cuando el compilador encuentra una plantilla instanciada, como Grid<int> myIntGrid, reemplaza cada T en la definición de clase de plantilla con un int, generando así el código para la versión int de la plantilla Grid. Cuando el compilador encuentra otra instancia de esta plantilla, genera otra versión de la clase Grid

2.3 Difundir código de plantilla en varios archivos

  • Por lo general, las definiciones de clases se colocan en un archivo de encabezado y las definiciones de métodos en un archivo de código fuente. El código que crea o usa un objeto de clase incluirá el archivo de encabezado correspondiente a través de #include, y accederá a estos códigos de método a través del enlazador
  • Las plantillas no funcionan de esta manera . Dado que el compilador necesita usar estas "plantillas" para generar el código de método real para el tipo instanciado, en cualquier archivo de código fuente que use plantillas, el compilador debe tener acceso tanto a la definición de clase de plantilla como a la definición de método . Hay varios mecanismos para satisfacer este requisito de contención.
2.3.1 Poner la definición de la plantilla en el archivo de cabecera
  • Las definiciones de métodos se pueden colocar directamente en el mismo archivo de encabezado que la definición de clase. Cuando un archivo fuente que usa esta plantilla incluye este archivo a través de #include, el compilador tiene acceso a todo el código que necesita. Alternativamente, puede colocar la definición del método de plantilla en otro archivo de encabezado y luego incluir este archivo de encabezado a través de #include en el archivo de encabezado de la definición de clase.
    // Grid.h
    template <typename T>
    class Grid {
          
          
        // ...
    };
    // 一定要保证方法定义的 #include 在类定义之后,否则代码无法编译
    #include "GridDefinition.h"
    
2.3.2 Poner la definición de la plantilla en el archivo fuente
  • Las definiciones de métodos se pueden colocar en un archivo de código fuente. Sin embargo, el código que utiliza la plantilla aún debe poder acceder a la definición, por lo que el archivo de origen para la implementación del método de clase se puede incluir en el archivo de encabezado de definición de clase de plantilla mediante #include
    // Grid.h
    template <typename T>
    class Grid {
          
          
        // ...
    };
    
    #include "Grid.cpp"
    

2.4 Parámetros de la plantilla

2.4.1 Parámetros de plantilla que no son de tipo
  • Los parámetros de plantilla que no son de tipo solo pueden ser tipos enteros (char, int, long, etc.), tipos de enumeración, punteros, referencias y std::nullptrt. A partir de C++ 17, también puede especificar auto, auto& y auto* como el tipo de parámetros de plantilla que no son de tipo. En este momento, el compilador deducirá automáticamente el tipo
    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 Valores predeterminados para parámetros de tipo
  • Si continúa usando la altura y el ancho como parámetros de plantilla, es posible que deba proporcionar valores predeterminados para la altura y el ancho (que no son parámetros de plantilla de tipo)
    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 Deducción de parámetros de plantilla para constructores
  • C++17 agregó funciones para admitir la deducción automática de argumentos de plantilla a partir de argumentos pasados ​​a constructores de plantillas de clase . Antes de C++17, todos los parámetros de plantilla debían especificarse explícitamente para las plantillas de clase
  • Por ejemplo, la biblioteca estándar tiene una plantilla de clase std:pair (definida en <utility>). pair almacena dos valores de dos tipos diferentes, que deben especificarse como parámetros de plantilla
    std::pair<int, double> pair1(1, 2.3);
    
  • Para evitar la necesidad de escribir parámetros de plantilla, está disponible una plantilla de función auxiliar std:make_pair(). Las plantillas de función siempre han admitido la deducción automática de argumentos de plantilla en función de los argumentos pasados ​​a la plantilla de función. Por lo tanto, make_pair() puede deducir automáticamente el parámetro de tipo de plantilla del valor que se le pasa.
    auto pair2 = std::make_pair(1, 2.3);
    
  • En C++17, estas plantillas de funciones auxiliares ya no son necesarias y el compilador puede deducir automáticamente los parámetros de tipo de plantilla en función de los parámetros reales pasados ​​al constructor.
    std::pair pair3(1, 2.3);
    


    La premisa de la deducción es que todos los parámetros de plantilla de la plantilla de clase tienen valores predeterminados o se usan como parámetros en el constructor. ) crear

2.5 Plantillas de métodos

  • C++ permite métodos individuales en clases con plantilla, ya sea en plantillas de clase o en clases sin plantilla

    No se pueden escribir métodos virtuales y destructores usando plantillas de métodos

  • Un objeto de tipo Grid<int> no se puede asignar a un objeto de tipo Grid<doble>, ni se puede construir un Grid<doble> a partir de un Grid<int>

  • Tanto el constructor de copia de Grid como el operator= toman una referencia a una const <Grid> como parámetro

    Grid(const Grid<T>& src);
    Grid<T>& operator=(const Grid<T>& rhs);
    
  • Al instanciar Grid<double> e intentar llamar al constructor de copia y al operador=, el compilador genera métodos a partir de estos prototipos.

    Grid(const Grid<double>& src);
    Grid<double>& operator=(const Grid<double>& rhs);
    
  • En la clase Grid<doble> generada, ni el constructor ni el operador aceptan Grid<int> como un parámetro, pero se puede resolver con una plantilla doble : agregar un constructor de copia con plantilla y un operador de asignación a la clase Grid puede generar métodos para convertir de Un tipo de malla a otro

    template <typename T>
    class Grid {
          
          
    public:
        // ...
        template <typename E>
        Grid(const Grid<E>& src);
    
        template <typename E>
        Grid<T>& operator=(const Grid<E>& rhs);
    };
    
    • La siguiente es la definición del nuevo constructor de copia. La línea que declara la plantilla de clase (con el parámetro T) debe colocarse antes de la línea que declara la plantilla de miembro (con el parámetro E)
    template <typename T>
    template <typename E>
    Grid<T>::Grid(const Grid<E>& src) : Grid(src.getWidth(), src.getHeight()) {
          
          
        // ...
    }
    

2.6 Especialización de plantillas de clase

  • Otra implementación de plantillas se denomina especialización de plantillas, a través de la cual se puede escribir una implementación especial para una plantilla cuando el tipo de plantilla se reemplaza por un tipo específico.
  • Al escribir una especialización de clase de plantilla, debe indicar que se trata de una plantilla y el tipo específico para el que se escribe la plantilla. Aquí está la especialización para const char*
    // 下述语法告诉编译器,这个类是 Grid 类的 const char* 特例化版本
    template <>
    class Grid<const char*> {
          
          
        // ...
    };
    
    Grid<int> myIntGrid;
    Grid<const char*> stringGrid(2, 2);
    

    Tenga en cuenta que en esta especialización, no especifique ninguna variable de tipo, como T, sino que maneje directamente const char*

  • El principal beneficio de la especialización es que se puede ocultar al usuario : cuando el usuario crea un Grid de tipo int o SpreadsheetCell, el compilador genera código a partir de la plantilla de Grid original; cuando el usuario crea un Grid de tipo const char*, el el compilador usa un caso especial de const char* Versión optimizada, todo esto se hace automáticamente en segundo plano
  • Cuando especializa una plantilla, no hereda ningún código: la especialización, a diferencia de la derivación, debe reescribir toda la implementación de la clase y no es necesario proporcionar métodos con el mismo nombre o comportamiento.
    • Por ejemplo, el caso especial const char* de Grid solo implementa el método at(), devolviendo std::opcional<std::cadena> en lugar de std::opcional<const char*>

2.7 Derivado de una plantilla de clase

  • Derivado de una plantilla de clase. Si una clase derivada hereda de la propia plantilla, entonces la clase derivada también debe ser una plantilla . Además, una instancia específica se puede derivar de una plantilla de clase, en cuyo caso la clase derivada no necesita ser una plantilla.
  • Digamos que la clase Grid genérica no proporciona suficiente funcionalidad de tablero. Específicamente, el método move() se agrega al tablero, lo que permite que las piezas del tablero se muevan de una posición a otra. A continuación se muestra la definición de clase para esta plantilla GameBoard
    • Esta plantilla GameBoard se deriva de la plantilla Grid, por lo que hereda todas las funciones de la plantilla Grid.
    • La sintaxis de la herencia es la misma que la de la herencia ordinaria, la diferencia es que la clase base es Grid<T> en lugar de 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 Herencia o especialización

  • Extienda la implementación y use el polimorfismo a través de la herencia, y personalice la implementación de tipos específicos a través de la especialización
    inserte la descripción de la imagen aquí

3. Plantillas de funciones

  • También se pueden escribir plantillas para funciones individuales. Por ejemplo, puede escribir una función genérica que busque un valor en una matriz y devuelva el índice de ese valor
    // 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;
    }
    
  • Al igual que las definiciones de métodos de plantilla de clase, las definiciones de plantilla de función (no solo los prototipos) deben estar disponibles en todos los archivos de origen donde se utilizan. Por lo tanto, si varios archivos de origen usan una plantilla de función o usan instanciación explícita como se explicó anteriormente, coloque su definición en el archivo de encabezado.
  • Los parámetros de plantilla de las plantillas de funciones pueden tener valores predeterminados, al igual que las plantillas de clase

3.1 Especialización de plantillas de funciones

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 Más introducción a la deducción de argumentos de plantilla

  • El compilador deduce el tipo del parámetro de plantilla de acuerdo con el parámetro real pasado a la plantilla de función y, para el parámetro de plantilla que no se puede deducir, debe especificarse explícitamente . Por ejemplo, la siguiente plantilla de función add() requiere tres parámetros de plantilla: el tipo del valor de retorno y los tipos de los dos operandos
    template <typename RetType, typename T1, typename T2>
    RetType add(const T1& t1, const T2& t2) {
          
          
        return t1 + t2;
    }
    
  • Al llamar a esta plantilla de función, puede especificar los tres parámetros de la siguiente manera
    auto result = add<long long, int, int>(1, 2);
    
  • Pero debido a que los parámetros de plantilla T1 y T2 son parámetros de la función, el compilador puede deducir estos dos parámetros, por lo que al llamar a add(), solo puede especificar el tipo del valor de retorno
    auto result = add<long long>(1, 2);
    
  • También puede proporcionar un valor predeterminado para el parámetro de plantilla de tipo de devolución, de modo que pueda llamar a add() sin especificar ningún tipo
    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 Tipos de devolución de plantillas de funciones

  • ¿No sería mejor dejar que el compilador deduzca el tipo del valor de retorno? De hecho, es bueno, pero el tipo de devolución depende del parámetro de tipo de plantilla, a partir de C ++ 14, puede pedirle al compilador que deduzca automáticamente el tipo de devolución de la función.
    template <typename T1, typename T2>
    auto add(const T1& t1, const T2& t2) {
          
          
        return t1 + t2;
    }
    
  • Sin embargo, cuando se usa auto para deducir el tipo de expresión, se eliminan los calificadores de referencia y const.Después de C++ 14, la función add() se puede escribir usando decltype(auto) para evitar eliminar cualquier calificador de referencia y const.
    template <typename T1, typename T2>
    decltype(auto) add(const T1& t1, const T2& t2) {
          
          
        return t1 + t2;
    }
    

4. Plantillas variables

  • Además de las plantillas de clase, las plantillas de método de clase y las plantillas de función , C++14 agrega la capacidad de escribir plantillas variables .
    template <typename T>
    constexpr T pi = T(3.14159265);
    
  • Lo anterior es una plantilla variable para el valor pi. Para obtener el valor de pi en un cierto tipo, se puede usar la siguiente sintaxis
    float piFloat = pi<float>;
    long double piLongDouble = pi<long double>;
    
  • Esto siempre dará como resultado una aproximación de pi que sea representable en el tipo solicitado. Al igual que otros tipos de plantillas, las plantillas variadas también se pueden especializar

Supongo que te gusta

Origin blog.csdn.net/qq_42994487/article/details/131433144
Recomendado
Clasificación