大话C++:第26篇 类模板

1 类模板定义

类模板是一个参数化类型,它使用一个或者多个参数来创建一系列类。类模板可以定义数据成员和函数成员,也可以使用访问标号控制对成员的访问,还可以定义构造函数和析构函数等。在类和类成员的定义中,可以使用模板形参作为类型或值的占位符,在使用类时再提供那些类型或值的具体信息。由于类模板包含类型参数,因此又称为参数化的类。利用类模板可以建立支持各种数据类型的类。

类模板的语法格式

cpp 复制代码
template <typename 类型参数1, typename 类型参数2, ..., typename 类型参数N>
class 类名 
{
    // 类成员声明和定义
};

其中,

  • template 关键字后面跟着尖括号 < >,尖括号内部是类模板的类型参数列表。

  • 每个类型参数由关键字 typename(或者 class,在模板参数中它们可以互换使用)后跟一个用户自定义的标识符组成。

2 类模板实例化

类模板实例化是C++模板编程中的一个关键步骤,它涉及将类模板的通用定义转换为特定类型的具体类的过程。类模板的实例化可以有两种方式:隐式实例化和显式实例化。

2.1 隐式实例化

类模板隐式实例化是C++模板编程中的一个概念,指的是在使用类模板时,编译器自动根据上下文信息为模板参数推导出具体类型,并生成相应的模板类的实例。这个过程是隐式的,因为程序员不需要显式地指定模板参数的类型,编译器会根据使用情况自动进行推导和实例化。

cpp 复制代码
#include <iostream>

// 定义一个简单的类模板
template<typename T>
class MyContainer 
{
public:
    MyContainer(T value) : content(value) 
    {
    }

    void Show() const 
    {
        std::cout << "Value: " << content << std::endl;
    }

private:
    T content;
};

int main() 
{
    // 隐式实例化 MyContainer<int>
    MyContainer<int> intContainer(42);
    intContainer.Show(); // 输出 "Value: 42"

    // 隐式实例化 MyContainer<double>
    MyContainer<double> doubleContainer(3.14);
    // 输出 "Value: 3.14"
    doubleContainer.Show(); 

    // 隐式实例化 MyContainer<std::string>
    MyContainer<std::string> stringContainer("Hello, World!");
    // 输出 "Value: Hello, World!"
    stringContainer.Show(); 

    return 0;
}

隐式实例化的主要过程:

  • 定义对象时 :当使用类模板来定义一个对象时,如果提供了足够的上下文信息,编译器可以推导出模板参数的类型,并隐式地实例化类模板。例如,MyContainer<int> obj; 这里虽然没有显式地请求实例化,但编译器知道需要 MyContainerint 类型实例,因此会隐式实例化。

  • 函数调用时:对于类模板成员函数,如果在调用时提供了足够的类型信息,编译器也可以隐式地实例化类模板。这通常发生在成员函数依赖于模板参数类型的情况下。

  • 类型推导 :在C++11及以后的版本中,引入了auto关键字和类型推导机制。当使用auto来声明一个由类模板生成的对象的变量时,编译器会利用类型推导来隐式地实例化类模板。

  • 模板参数默认类型:如果类模板定义了默认模板参数,那么在特定情况下,编译器可以使用这些默认参数来隐式地实例化类模板。

注意,隐式实例化是在需要时才发生的,也就是说,只有当代码实际使用到类模板的某个具体实例时,编译器才会去生成相应的代码。此外,如果类模板的某个成员函数没有被使用到,那么即使类模板被实例化,这个成员函数也不会被实例化。

2.2 显式实例化

类模板的显式实例化(explicit instantiation)是我们明确告诉编译器在此时此地实例化一个类模板的过程。这通常涉及在模板定义之外的某个源文件中,使用特定的语法来请求编译器生成模板的一个或多个具体实例。

显式实例化的语法格式:

cpp 复制代码
template class 类名<具体类型>;

对于类模板的成员函数,显式实例化语法格式

cpp 复制代码
template void 类名<具体类型>::成员函数名(参数类型);

定义一个简单的类模板 MyArray,封装了一个固定大小的数组,并提供了一些基本操作,具体代码示例

cpp 复制代码
// my_array.h
#ifndef __MY_ARRAY_H
#define __MY_ARRAY_H

#include <iostream>

template<typename T, std::size_t Size>
class MyArray 
{
public:
    MyArray() = default;

    T& operator[](std::size_t index) 
    {
        return data[index];
    }

    const T& operator[](std::size_t index) const 
    {
        return data[index];
    }

    std::size_t getSize() const 
    {
        return Size;
    }

private:
    T data[Size];
};

#endif // MY_ARRAY_H



// 一个单独的源文件中对 MyArray 进行显式实例化。
// 通常,显式实例化会放在与模板定义不同的源文件中,以避免在同一编译单元中多次实例化相同的模板
// my_array_instantiations.cpp
#include "my_array.h"

// 显式实例化 MyArray 的一些特定类型
// MyArray<int, 5>
template class MyArray<int, 5>;    
// MyArray<double, 10>
template class MyArray<double, 10>;   
// 可以继续为其他类型和大小进行显式实例化


// main.cpp
#include "my_array.h"
#include <iostream>

int main() 
{
    // 使用已经显式实例化的 MyArray 类型
    MyArray<int, 5> intArray;
    MyArray<double, 10> doubleArray;

    // 填充 intArray
    for (std::size_t i = 0; i < intArray.getSize(); ++i) 
	{
        intArray[i] = i * i;
    }

    // 打印 intArray
    std::cout << "Int Array:" << std::endl;
    for (const auto& element : intArray) 
    {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    // 填充 doubleArray
    for (std::size_t i = 0; i < doubleArray.getSize(); ++i) 
	{
        doubleArray[i] = i * 1.5;
    }

    // 打印 doubleArray
    std::cout << "Double Array:" << std::endl;
    for (const auto& element : doubleArray) 
    {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    return 0;
}

注意,确保在编译时包含了所有必要的源文件,这样编译器才能找到所有的模板定义和显式实例化。

3 模板类注意事项

C++中使用模板时,需要注意的事项:

  • 编译时间:模板的实例化是在编译时进行的,这可能导致编译时间显著增加,特别是当使用大量模板或模板嵌套时。

  • 代码膨胀:每个模板类型的实例化都会产生新的代码。如果模板被用于许多不同的类型,这可能导致生成的二进制文件大小显著增加。

  • 调试难度:由于模板是在编译时展开的,调试模板代码可能比普通代码更加困难。错误消息可能指向模板实例化点而不是实际的模板定义,这增加了定位问题的复杂性。

  • 可读性:复杂的模板元编程可能导致代码难以理解和维护。模板的语法和概念也可能对初学者来说较为晦涩。

  • 编译器差异:不同的编译器可能在模板支持、错误消息质量和编译时间上有所不同。这可能导致跨编译器的不一致性。

  • 模板参数

    • 类型参数:模板主要用于泛型编程,允许用户为多种类型编写相同的代码。但是,某些类型可能不适用于模板的通用实现,需要特别注意。

    • 非类型参数:除了类型,模板还可以接受常量表达式作为参数(如整数或枚举值),这增加了模板的灵活性,但也增加了复杂性。

  • 模板特化:虽然模板特化可以提供对特定类型的定制实现,但过度使用特化可能导致代码库的复杂性和维护负担增加。

  • 部分特化与默认参数:部分特化和默认模板参数都是减少代码重复和提高灵活性的工具,但也需要谨慎使用以避免混淆。

  • 模板与内联:模板函数通常是内联的候选者,因为它们在多个地方可能有相同的代码。然而,过度内联可能会增加生成的代码大小并影响性能。

  • 头文件组织:模板的声明和定义通常都放在头文件中,因为编译器在实例化模板时需要看到完整的定义。这可能导致头文件包含的问题和循环依赖。

  • 模板参数推导:C++11及更高版本提供了更强大的模板参数推导机制,这简化了模板的使用,但也需要注意推导规则以避免意外。

  • 模板与异常:在模板中使用异常需要特别小心,因为不同的类型可能有不同的异常规格和保证。

  • 前向声明与模板:对于类模板,通常不能仅通过前向声明来使用它们。编译器需要看到完整的模板定义才能实例化它。

  • 模板与友元:在模板类中声明友元函数或类需要特别注意,因为友元关系可能不会如预期的那样扩展到所有模板实例化。

相关推荐
金士顿2 小时前
MFC 文档模板 每个文档模板需要实例化吧
c++·mfc
人才程序员5 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面
w(゚Д゚)w吓洗宝宝了5 小时前
C vs C++: 一场编程语言的演变与对比
c语言·开发语言·c++
小老鼠不吃猫7 小时前
C++点云大文件读取
开发语言·c++
姚先生978 小时前
LeetCode 35. 搜索插入位置 (C++实现)
c++·算法·leetcode
CoderCodingNo8 小时前
【GESP】C++二级考试大纲知识点梳理, (4)流程图
开发语言·c++·流程图
小小unicorn8 小时前
【C++初阶】STL详解(十三)—— 用一个哈希表同时封装出unordered_map和unordered_set
java·c++·散列表
慕羽★9 小时前
详细介绍如何使用rapidjson读取json文件
linux·c++·windows·json·file·param·rapidjson
大梦百万秋9 小时前
C++中的虚拟化:通过虚拟机管理模拟服务器虚拟化
服务器·开发语言·c++
shentuyu木木木(森)9 小时前
入门STL(map/multiset)
开发语言·数据结构·c++·算法·map·multiset