[C++] 模板进阶:特化与编译链接全解析

Kevin的技术博客.png

文章目录

  • 非类型模板
    • 类型形参
    • 非类型模板参数
    • 代码示例
  • **模板的特化**
    • 为什么要有模板的特化
    • 函数模板特化
      • 使用场景与示例
      • 函数模板特化的实现细节
    • 类模板特化
      • 全特化
          • 示例
      • 偏特化
        • 部分优化
        • 通过进一步限制模板参数进行特化
          • 偏特化为指针类型示例:
          • 偏特化为引用类型示例:
    • 特化测试结果分析
  • 模板特化中的注意事项
    • 实例化时严格的匹配性
    • 指针特化时`const`的修饰问题
      • 为什么在参数列表使用`const`?
      • `const`与指针修饰关系
        • **指针本身是常量** (`const`在`*`之后)
        • **指向的内容是常量** (`const`在`*`前面)
      • 指针特化时`const`修饰的应用
    • 已经特化的类中`T`表示为什么?
  • 模板的分离编译
    • 分离编译模式简介
    • 模板的分离编译
      • 分离编译测试
      • 原因解析
        • C/C++程序的编译链接原理
        • 为什么不能分离定义?


非类型模板

模板参数分为:类型形参和非类型形参

类型形参

类型形参,即在模板初阶中所用的例如class Atypename A此类参数类型,跟在classtypename后。
[C++] 模版初阶-CSDN博客

非类型模板参数

非类型模板参数,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用,定义方法如下:

template<class T, size_t N = 10>

注意:

  1. 非类型模板参数只能是整型。
  2. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  3. 非类型的模板参数必须在编译期就能确认结果(原因看下文)。

代码示例

template<int N>
class Array {
public:int arr[N];// 其他方法
};
Array<5> myArray; // 创建一个包含5个元素的数组对象

模板的特化

为什么要有模板的特化

模板技术提供了强大的泛型编程能力,使得我们能够编写与数据类型无关的代码,从而提高代码的复用性和灵活性。然而,在实际应用中,有时需要对特定类型进行特殊处理,这时就需要用到模板特化

**注意:**一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出

模板特化的出现是为了解决模板在处理某些特殊类型时可能遇到的问题。例如,一个通用的比较函数模板可以比较大多数类型的数据,但在遇到指针时,仅比较指针的地址而不是指向的内容,这就可能导致错误的结果。模板特化允许为特定类型提供定制的实现,以解决这些特殊情况下的需求。

// 例如日期类中的函数模板的使用,在使用指针比较的时候就会出现错误,这时候就需要进行模板特化
template<class T>
bool Less(T left, T right)
{return left < right;
}Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // 可以比较,结果正确Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 可以比较,结果错误

当使用指针进行比较的时候比较的就是指针指向的地址,而地址是从栈上向下申请,所以不会按照原本日期类希望的排序方法进行排序。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化

函数模板特化

函数模板特化用于为特定类型定制函数实现。它的典型用处是在普通模板无法满足某些类型需求时提供特定的功能。特化函数的签名必须与原模板函数完全一致。

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板;
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型;
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇
    怪的错误。

使用场景与示例

紧接上面的错误案例:假设我们有一个通用的比较函数模板Less,它比较两个对象的大小

template<typename T>
bool Less(T left, T right) {return left < right;
}

上述模板在大多数情况下都能正常工作,但如果传入的是指针类型,那么比较的将是指针的地址而非指向的对象。为了正确比较指针指向的内容,我们需要对指针类型进行特化:

template<>
bool Less<Date*>(Date* left, Date* right) {return *left < *right;
}

此特化版本用于比较指针指向的Date对象,确保比较逻辑正确。

函数模板特化的实现细节

在实现函数模板特化时,需要注意以下几点:

  • 特化声明:模板特化的声明需要紧随template<>,然后是函数签名,特化的类型需要放在尖括号中。
  • 参数一致性:特化函数的参数列表必须与原模板函数保持一致,不能增加或减少参数,也不能更改参数的顺序或类型。

**注意:**推荐直接写一个函数实现特殊处理,编译器在处理的时候会优先调用更匹配的。

类模板特化

类模板特化比函数模板特化更加复杂,主要分为全特化偏特化。类模板特化的主要作用是为特定类型提供定制的类定义和实现。

全特化

全特化是指将模板参数列表中的所有参数都具体化(全特化版本中的所有参数都必须指定具体类型)。

示例

假设我们有一个通用的数据存储类模板Data,它可以存储两个不同类型的对象:

template<typename T1, typename T2>
class Data {
public:Data() { std::cout << "Data<T1, T2>" << std::endl; }
private:T1 _d1;T2 _d2;
};

我们可以为特定的类型组合如intchar进行全特化:

template<>
class Data<int, char> {
public:Data() { std::cout << "Data<int, char>" << std::endl; }
private:int _d1;char _d2;
};

在这个全特化版本中,我们为Data类提供了一个int和一个char类型的特化实现。这意味着当我们创建Data<int, char>类型的对象时,将调用特化版本的构造函数。

偏特化

偏特化是部分特化的形式,可以仅对部分模板参数进行特化。偏特化比全特化更灵活,允许特化的同时保留一些模板参数。

偏特化中有两种表现方式:部分特化、通过限制参数进行特化

部分优化

部分特化允许开发者针对特定的模板参数进行特化,而其他模板参数保持泛型(需要在template中声明)。这样可以在不影响通用模板行为的情况下,为某些特定类型或类型组合提供专门的实现。

示例:

template<typename T1, typename T2>
class Pair {
public:Pair(T1 first, T2 second) : first_(first), second_(second) {}T1 first() const { return first_; }T2 second() const { return second_; }private:T1 first_;T2 second_;
};

上述模板类是通用的,可以存储任意类型的两个数据。然而,如果我们需要对第一类型是int的情况进行特化,可以使用部分特化:

template<typename T2>
class Pair<int, T2> {
public:Pair(int first, T2 second) : first_(first), second_(second) {}int first() const { return first_; }T2 second() const { return second_; }void setFirst(int value) { first_ = value; } // 额外的特化方法private:int first_;T2 second_;
};

在这个部分特化版本中,我们特化了Pair模板的第一个类型为int,第二个类型保持泛型。这样,当Pair<int, T2>的对象创建时,将调用这个特化版本,而不是通用版本。

通过进一步限制模板参数进行特化
偏特化为指针类型示例:

当需要模板参数为指针类型的时候,可以对其进行特化,以实现针对于指针的特定逻辑。这在需要对指针执行特定操作(如解引用、比较等)时尤为有用。

// 两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data<T1*, T2*> {
public:Data() { std::cout << "Data<T1*, T2*>" << std::endl; }
private:T1 _d1;T2 _d2;
};
  • 模板特化Data<T1*, T2*>,这个偏特化版本对模板的两个参数T1T2进行了特化,使得它们必须是指针类型。
  • 实现细节:在构造函数中打印了一条消息,标识这是指针特化的版本。
  • 成员变量:特化类中的成员变量依然是T1T2类型,不过它们实际上是指针指向的对象的类型。
偏特化为引用类型示例:

对于引用类型的参数,我们可以通过特化来处理那些需要传递引用的情况。这在需要修改外部对象或避免对象复制时非常有用。

// 两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data<T1&, T2&> {
public:Data(const T1& d1, const T2& d2): _d1(d1), _d2(d2) {std::cout << "Data<T1&, T2&>" << std::endl;}
private:const T1& _d1;const T2& _d2;
};
  • 模板特化Data<T1&, T2&>,这个偏特化版本对模板的两个参数T1T2进行了特化,使得它们必须是引用类型。
  • 实现细节:在构造函数中接受了T1T2类型的引用,并初始化类的成员变量。
  • 成员变量:特化类中的成员变量是对传入对象的常量引用const T1&const T2&,这确保了数据不会被意外修改。

特化测试结果分析

void test2() {Data<double, int> d1; // 调用全特化的模板Data<int, double> d2; // 调用基础的模板Data<int*, int*> d3; // 调用特化的指针版本Data<int&, int&> d4(1, 2); // 调用特化的引用版本
}
  • Data<double, int> d1;:调用了全特化模板,因为参数类型既不是指针也不是引用。
  • Data<int, double> d2;:同样调用全特化模板。
  • Data<int*, int*> d3;:调用了特化的指针版本,因为两个参数都是指针类型。
  • Data<int&, int&> d4(1, 2);:调用了特化的引用版本,因为两个参数是引用类型(注意,这里初始化引用类型参数时传递的是常量12,这些字面量会被隐式转换为合适的引用类型)。

模板特化中的注意事项

实例化时严格的匹配性

模板编程中,模板实例化时的匹配性要求非常严格,即使已经对模板进行了特化,在实例化时也必须精确匹配到最合适的模板版本。这种严格的匹配性体现在以下几个方面:

  • 全特化:指的是为特定类型组合提供一个完全定制化的实现。全特化要求在实例化时完全匹配所有模板参数类型,只有在参数完全匹配时,才会使用该特化版本。
  • 偏特化:允许对部分模板参数进行特化,同时保持其他参数的泛型性。在实例化时,编译器会优先选择最匹配的特化版本。如果没有找到完全匹配的特化版本,编译器才会退而求其次,选择更加通用的版本。
  • 模板匹配顺序:编译器在选择模板实例化时,会按照以下优先顺序进行匹配:
    • 完全匹配的全特化(优先级最高)
    • 最匹配的偏特化
    • 最通用的模板

指针特化时const的修饰问题

为什么在参数列表使用const

防止修改传入的参数:特化版本中的Date* const& leftDate* const& right,通过使用const,函数保证不会修改传入的指针变量本身的值,即指针的指向保持不变。这是一种安全措施,避免函数对外部数据的不必要的修改。

const与指针修饰关系

指针本身是常量 (const*之后)

const放在指针符号*之后时,它修饰的是指针本身,这意味着指针的值(即它指向的内存地址)不能被改变。但指针指向的对象的内容可以改变。

Date* const pDate;

在这个例子中,pDate是一个常量指针,它指向一个Date类型的对象。pDate本身不能指向别处,但是pDate指向的Date对象的内容是可以修改的。

通过特化时将**const**放在在*****之后即可解决在特化中的修饰关系。

指向的内容是常量 (const*前面)

const放在*前面时,它修饰的是指针指向的对象,这意味着不能通过这个指针修改指向的对象的内容,但指针本身可以指向不同的对象。

const Date* pDate;

在这个例子中,pDate是一个指向Date对象的指针。虽然pDate本身可以指向不同的Date对象,但不能通过pDate来修改它所指向的对象的内容。

指针特化时const修饰的应用

通用函数模板

template<class T>
bool LessFunc(const T& left, const T& right)
{return left < right;
}

该函数模板中的const修饰的是传入的leftright不会被改变。

特化函数模板

template<>
bool LessFunc<Date*>(Date* const& left, Date* const& right)
{return *left < *right;
}

**Date* const& left****Date* const& right**:这两个参数都是指向Date对象的常量指针的引用。这意味着:

  • 指针本身不可改变:函数内部不能改变leftright指向的地址(与通用模板中的修饰目的相同)。

为了保持与通用模板中const效果相同,因此写为Date* const& left。通用模板是为了是传入的数据不被修改,而对于传入的指针来说,**const**放在*****之后,表示指针本身是常量。换句话说,指针本身的地址不能改变,也就是说,一旦初始化后,指针不能指向其他地址,也就是传入的指针不能被修改了,和通用模板实现的效果相同。
因此,Date* const& 的意思是“指向Date对象的常量指针的引用”。这个引用在函数内不会改变其所引用的指针对象,也不能通过引用修改指针本身的指向。

已经特化的类中T表示为什么?

在已经特化过的类中,不管特化时是将原类型特化为指针类型或者引用类型之类的,在类中使用T的时候一律会按照原类型进行使用,也就是说如果在类中要用原类型的指针类型的话,还是需要用T*

如此表示在const修饰传参时也有用处,例如上文所理解的LessFunc<Date*>(Date* const& left, Date* const& right),如果特化为指针的类中Date实际表示为Date*的话,那么在修饰的时候究竟要如何修饰呢。此时就会产生语法与习惯上的矛盾,所以将T直接作为原类型使用会更加方便与顺手。

模板的分离编译

分离编译模式简介

分离编译是软件工程中的一个基本概念,它指的是将源代码分割成多个模块,每个模块独立编译,最后通过链接器将这些模块组合成最终的可执行文件。这种方式提高了编译的并行性,同时也使得代码维护更加简单,因为修改一个模块通常不会影响到其他模块的编译。

模板的分离编译

分离编译测试

我们有一个模板函数Add,它的声明和定义被分别放在不同的文件中:

// a.h
template<class T>
T Add(const T& left, const T& right);// a.cpp
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}

main.cpp中,我们使用了Add函数的两个实例:

#include "a.h"int main()
{Add(1, 2); // 整数实例Add(1.0, 2.0); // 浮点数实例return 0;
}

此时当运行的时候会出现链接错误。

原因解析

C/C++程序的编译链接原理

C/C++程序的构建过程通常分为四个阶段:预处理、编译、汇编和链接。

  1. 预处理:预处理器处理#include指令和其他预处理器指令,将头文件的内容插入到源文件中,同时处理宏定义等。
  2. 编译:编译器将预处理后的源代码转换成汇编代码。在这个阶段,编译器检查语法、词法和语义错误,并且如果一切正确,将代码转换成机器可以理解的指令集。
  3. 汇编:将汇编代码转换为机器代码的二进制形式。
  4. 链接:链接器将多个目标文件(.obj)和库文件链接起来,解决符号引用问题,生成最终的可执行文件。

image.png

为什么不能分离定义?

**原因:**模板实例化的代码并不是编译的时候在模板位置直接生成的,而是在需要实例化的时候才会生成特定的具体代码。

  • 实例化时机:模板的实例化发生在编译器遇到模板函数或类的使用时。如果模板的定义不在编译器当前正在处理的编译单元中,那么编译器无法知道如何实例化模板,因此不会生成相应的函数代码。
  • 地址问题:如你提到的例子,当在a.cpp中没有Add模板的具体实例化代码时,编译器不会生成对应的函数。而在main.obj中尝试使用Add<int>Add<double>时,链接器会在链接阶段寻找这些函数的地址,但因为它们在编译时没有被生成,所以链接器找不到这些地址,导致链接错误。
  • 单定义规则(One Definition Rule,ODR):C++的单定义规则要求每个非内联函数或变量在一个程序中只能有一个定义。模板的每次实例化都被视为一个独立的函数或类型定义,这意味着每次实例化都必须在同一个编译单元中完成,否则可能会违反ODR。
  • **推荐做法:**将模板的声明和定义放在同一个头文件中,确保在任何包含该头文件的编译单元中都可以进行正确的实例化。


image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/393016.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

menuconfig+Kconfig的简单配置

目录 1.背景 2.管理方案 2.1&#xff1a;.h中直接定义 2.2&#xff1a;.batCmake 2.3&#xff1a;Kconfig 2.3.1 环境安装 2.3.2 代码 2.3.2.1 目录结构 2.3.2.2 ble目录下的Kconfig 2.3.2.3 hardware目录下的Kconfig 2.3.2.4 rtos目录下的Kconfig 2.3.2.5 根目录 …

申请专利需要准备哪些材料?

申请专利需要准备哪些材料&#xff1f;

实践致知第17享:电脑忽然黑屏的常见原因及处理方法

一、背景需求 小姑电话说&#xff1a;最近&#xff0c;电脑忽然就黑屏了&#xff08;如下图所示&#xff09;&#xff0c;但是等待几十秒甚至一分钟&#xff0c;电脑就能自然恢复了&#xff0c;这种状况一天能出现三四次&#xff0c;怎么办&#xff1f; 二、分析诊断 电脑黑屏…

keeplive配置详解与haproxy配置详解

一、keepalive相关知识 1.1 keepalive介绍 keepalive即LVS集群当中的高可用架构&#xff0c;只是针对调度器的高可用。是高可用的HA架构。 keepalive就是基于VRRP协议来实现LVS高可用的方案。 1、组播地址 224.0.0.18&#xff0c;根据组播地址进行通信&#xff0c;主备之间发…

Java多线程-----定时器(Timer)及其实现

目录 一.定时器简介&#xff1a; 二.定时器的构造方法与常见方法&#xff1a; 三.定时器的模拟实现&#xff1a; 思路分析&#xff1a; 代码实现&#xff1a; 在开发中&#xff0c;我们经常需要一些周期性的操作&#xff0c;例如每隔几分钟就进行某一项操作&#xff0c;这…

标准IO——文件定位、文件IO

续&#xff1a;feof、ferror&#xff08;检测一个流是否出错&#xff09;、clearerr&#xff08;清除一个流出错的标记&#xff09;。 一、标准IO文件定位 1、fseek(定位&#xff09; int fseek(FILE *stream , long offset(偏移长度) , int whence(偏移起始位置)) 其中when…

阿里云SMS服务C++ SDK编译及调试关键点记录

一. 阿里云SMS服务开通及准备工作 在阿里云官网上完成这部分的工作 1. 申请资质 个人or企业 我这里是用的企业资质 2. 申请签名 企业资质认证成功后&#xff0c;会自动赠送一个用于测试的短信签名 也可以自己再进行申请&#xff0c;需要等待审核。 3. 申请短信模板 企…

还没用过OBS Studio?快来提升你的技术分享效率!

前言 在浩瀚的数字海洋中&#xff0c;有这么一款神器&#xff0c;它低调却光芒四射&#xff0c;默默改变着无数内容创作者的命运&#xff1b;嘿&#xff0c;你猜怎么着&#xff1f;它既不是天价的专业设备&#xff0c;也不是遥不可及的神秘黑科技&#xff0c;而是开源世界的瑰宝…

本地Gitlab-runner自动编译BES项目

0 Preface/Foreword 1 Gitlab-runner配置情况 具体情况如下&#xff1a; Gitlab-ruuner运行在wsl 1中的Ubuntu 18.04 distro上专门为GitLab-runner分配了一个用户&#xff0c;名为gitlab-runner 2 自动编译 2.1 找不到编译工具链 根据错误提示&#xff0c;交叉编译工具链未找…

深入理解接口测试:实用指南与最佳实践(四)IHRM管理系统实战-项目分析

​ ​ 您好&#xff0c;我是程序员小羊&#xff01; 前言 这一阶段是接口测试的学习&#xff0c;我们接下来的讲解都是使用Postman这款工具&#xff0c;当然呢Postman是现在一款非常流行的接口调试工具&#xff0c;它使用简单&#xff0c;而且功能也很强大。不仅测试人员会使用…

牛!手机上轻松部署大模型全攻略!

当前AI革命中&#xff0c;大模型发挥关键角色&#xff0c;其理论基础在于Scaling Law。简单来说就是&#xff0c;随着数据、参数和计算能力的提升&#xff0c;模型能力增强&#xff0c;展现出小规模模型所不具备的“涌现能力”。众多AI企业推出开源大模型&#xff0c;规模按扩展…

红黑树的概念和模拟实现[C++]

文章目录 红黑树的概念一、红黑树的性质红黑树原理二、红黑树的优势和比较 红黑树的模拟实现构建红黑树的数据结构定义节点的基本结构和初始化方式插入新节点插入新节点的颜色调整颜色和结构以满足红黑树性质 红黑树的应用场景 红黑树的概念 一、红黑树的性质 红黑树是一种自平…

Redis系列之Redis Sentinel

概述 Redis主从集群&#xff0c;一主多从模式&#xff0c;包括一个Master节点和多个Slave节点。Master负责数据的读写&#xff0c;Slave节点负责数据的查询。Master上收到的数据变更&#xff0c;会同步到Slave节点上实现数据的同步。通过这种架构实现可以Redis的读写分离&…

工具|阅读PDF时鼠标显示为小手中有向下箭头解决方法

由于工作中&#xff0c;会大量阅读PDF文档&#xff0c;如手册&#xff0c;规格书&#xff0c;各种图纸等&#xff0c;因此好用的PDF工具必不可少。我主要习惯用福昕阅读器&#xff0c;标注比较方便。 所以&#xff0c;本文主要以福昕阅读器为主&#xff0c;当然也适用于其他的阅…

Docker Volume(存储卷)

一、认识 1.1 概念 存储卷就是将宿主机的本地文件系统中存在的某个目录直接与容器内部的文件系统上的某一目录建立绑定关系。这意味着&#xff0c;在容器中的这个目录下写入数据时&#xff0c;容器会将内容直接写入到宿主机上与此容器建立了绑定关系的目录 在宿主机上的这个…

实验8-1-6 在数组中查找指定元素

本题要求实现一个在数组中查找指定元素的简单函数。 函数接口定义&#xff1a; int search( int list[], int n, int x );其中list[]是用户传入的数组&#xff1b;n&#xff08;≥0&#xff09;是list[]中元素的个数&#xff1b;x是待查找的元素。如果找到 则函数search返回…

基于Golang实现Kubernetes边车模式

本文介绍了如何基于 Go 语言实现 Kubernetes Sidecar 模式&#xff0c;并通过实际示例演示创建 Golang 实现的微服务服务、Docker 容器化以及在 Kubernetes 上的部署和管理。原文: Sidecar Pattern with Kubernetes and Go[1] 在这篇文章中&#xff0c;我们会介绍 Sidecar 模式…

软件测试学习笔记

测试学习 1. 测试流程2. Bug的提出什么是bugbug 的描述bug 级别 3. 测试用例的设计什么是测试用例测试用例应如何设计基于需求的设计方法等价类边界值场景法正交表法判定表法错误猜测法 4. 自动化测试回归测试自动化分类 5. 安装 webdriver-manager 和 selenium第一个web自动化…

链表List

简介 STL中的List与顺序表vector类似&#xff0c;同样是一种序列式容器&#xff0c;其原型是带头节点的双向循环链表。 List的使用 list中的接口比较多&#xff0c;此处类似&#xff0c;只需要掌握如何正确的使用&#xff0c;然后再去深入研究背后的原理&#xff0c;已达到可…

基于R语言生物信息学大数据分析与绘图

随着高通量测序以及生物信息学的发展&#xff0c;R语言在生物大数据分析以及数据挖掘中发挥着越来越重要的作用。想要成为一名优秀的生物数据分析者与科研团队不可或缺的人才&#xff0c;除了掌握对生物大数据挖掘与分析技能之外&#xff0c;还要具备一定的统计分析能力与SCI论…