整理C++模板的语法

2024-09-06 23:38
文章标签 模板 c++ 整理 语法

本文主要是介绍整理C++模板的语法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

C++模板

【模板】 是C++实现泛型编程的一种手段。泛型编程的目的,说白了就是对逻辑进行“复用”来减少重复。换句话说:它将针对于特定类型的逻辑,抽象成了纯粹的逻辑,这样就可以适用于广泛的类型。当然,越抽象的东西是越难理解,C++【模板】的理解成本自然不低。最近在看UE4代码时也遇到了大量关于模板的花式使用,很令我头疼。不过我明白,再复杂的使用,也是基于一些最简单的语法规则的,因此我想有必要总结下C++模板的语法,以帮助我阅读那些“高级”的代码。

主要的参考是官方文档:《模板 (C++) | Microsoft Docs》。不过我想用自己的思路来整理这些知识。

模板牵扯到的问题

概括来讲,问题分为两方面:“定义模板”,“使用模板”


定义模板的主要问题:

1)模板参数如何指定?
模板参数可以看做是一种类型的“占位符”。但是,C++语法中,这里的模板参数还有更高级的用途,随后讨论。

2)模板的特化
有时候,想让逻辑适应于一个“广泛”的情况,但是想让这个逻辑在“特定”情况下有特别的表现,这时候就可以就需要模板特化


使用模板的主要问题:

1)省略模板参数让其自动推导
在一些情况下,模板的参数是可以推导出来的,此时就不必显式地指定模板参数了。

2)编译器将选择哪个版本的模板?
有时候,会有超过一种的模板符合格式。此时,编译器将会基于一定的规则去选择。使用者必须知道这个规则,这样才能确保调用到想要的那个版本。


虽然,接下来可以按照上面的问题来逐个讨论具体的细节,但也可以按照“从易到难、从简到繁”的语法点来讨论,我觉得这样更有利于未来的查阅和理解,因此我选择后者。

1. 类型参数

这是C++模板最简单的一个使用:

#include<iostream>
using namespace std;template <typename T>
T min(T a, T b)
{return a < b ? a : b;
}int main()
{cout << min<float>(4.3, 9.6) << endl;
}

输出:

4.3

在这个例子中,T是一个模板参数,更精确来讲是一个类型参数,而min就是一个“模板函数”,实现了一个得到二者中较小值的逻辑。


在此情境中,classtypename等价。因此以下的语句是等价的:

template <class T>

2. 类型参数的自动推导

在上例中,<float>其实是可以省略的,因为函数参数的类型编译器是知道的,那么就可以推导出模板参数也是这个类型,所以使用时可以省略:

int main()
{cout << min(4.3, 9.6) << endl;
}

3. 非类型参数(值参数)

C++中的模板参数并不一定是类型参数,还有可能是非类型参数(值参数),例如:

template <int value>
void Test()
{cout << value << endl;
}int main()
{Test<3>();
}

输出:

3

看起来和一般函数的“参数”类似,但是实际它将受到很大的限制:
1)必须是编译时常量
这很自然,毕竟C++模板是在编译时实例化的,是“静态”的。
如果尝试挑战这个限制,比如调用函数得到结果,则会报错:
在这里插入图片描述
2)类型上受限制
intboolenum经试验是可以的。
但并不是所有类型都可以使用,比如使用float,就会报错:
在这里插入图片描述
自定义的结构体/类更是不行
在这里插入图片描述
但是指针是可以的,例如:

struct MyStruct
{
};template <MyStruct* value>
void Test()
{
}

这方面我并没有找到权威文档完整描述什么是可以什么是不行的,唯一的描述在这个官方文档中:

与其他语言(如 c # 和 Java)中的泛型类型不同,c + + 模板支持非类型参数(也称为值参数)。 例如,你可以提供常量整数值来指定数组的长度。其他类型的值(包括指针和引用)可以作为非类型参数传入。 例如,你可以传入指向函数或函数对象的指针,以自定义模板代码内的某些操作。

4. 模板参数的默认

类型参数非类型参数都可以指定默认值,如:

struct MyStruct
{char a;char b;
};template <typename T = MyStruct, int Value = 8>
void test()
{cout << sizeof(T) << endl;cout << Value << endl;
}int main()
{test();
}

输出:

2
8

上例中,使用test()时并没有指定模板参数,因此使用了默认值,输出了MyStruct的尺寸:2,和Value的默认值8

5. 模板的特化

template <typename T >
void test(T t)
{cout << "泛化版本" << endl;
}template <>
void test<int>(int t)
{cout << "int特化版本" << endl;
}int main()
{test(3.3f);test('c');test(3);
}

输出:

泛化版本
泛化版本
int特化版本

在这个例子中,一般的模板函数定义之后,还专门对int这种类型定义了一个“特别”的版本,这就是模板特化

6. 与非模板函数的选择

当有一个语句同时和“模板函数”与一个“非模板函数”都匹配,则编译器会选择那个“非模板函数”,因为它认为这个“非模板函数”更专业,更匹配于特定想要解决的问题。除非特别地使用模板的语法来显式调用。详见下例:

template <typename T >
void test(T t)
{cout << "泛化版本" << endl;
}template <>
void test<int>(int t)
{cout << "int特化版本" << endl;
}void test(int t)
{cout << "非模板test函数" << endl;
}int main()
{test(3.3f);test('c');test(3);test<int>(3);
}

输出:

泛化版本
泛化版本
非模板test函数
int特化版本

此问题其实是一个函数重载问题,在官方文档中也有讨论这个问题。

7. 类模板

模板类模板函数类似:

template <typename T >
class TestClass
{
};

在一个模板类中的函数都视作是模板函数

template <typename T >
class TestClass
{void test();
};template<typename T > 
void TestClass<T>::test()
{
}

就算函数中没有用到模板参数相关的内容,也必须指出模板参数,否则会报错:
在这里插入图片描述


模板类中的函数还可以额外再指定模板参数:

template <typename T >
class TestClass
{template <typename U >void test();
};template <typename T > template <typename U >
void TestClass<T>::test()
{
}

8. 依赖模板参数的名称解析

“依赖模板参数的名称” 主要包括:

1)模板类型参数本身:

T

2)模板类型参数命名空间的类型:

T::myType

2)基于依赖类型的指针、引用、数组或函数指针类型:

T *, T &, T [10], T (*)()

…)
此部分更完整的讨论可参考《模板和名称解析 | Microsoft Docs》和《依赖类型的名称解析 | Microsoft Docs》这两个官方文档。

下面是实例:

struct MyStruct
{int data;struct InnerStruct{float data2;};
};template<typename T>
void test()
{T value;T* ptr;value.data = 3;typename T::InnerStruct value2;value2.data2 = 3.3f;
}int main()
{test<MyStruct>();
}

9. 模板作为模板参数

模板也可以作为另一个模板的模板参数
例如:

template<typename T, typename U>
class MyStruct
{
public:T v1;U v2;void func(){cout << v1 << endl;cout << v2 << endl;}
};template<typename T, template<typename, typename> typename S>
void test(T value)
{S<T, float> s;s.v1 = value;s.v2 = 6.9f;s.func();
}int main()
{test<int, MyStruct>(3);
}

输出:

3
6.9

上例中,MyStruct这个模板类作为了test()这个模板函数的第二个模板参数

10. 模板的“专业程度”

当有超过1个模板可以匹配时,编译器会选择 “专业程度” 最高的版本。

“专业程度”的高低可以这样判断:
设满足T1模板参数的所有有效参数类型为集合1,设满足T2模板参数的所有有效参数类型为集合2,如果集合1集合2的子集,则表明T1模板参数“专业程度”更高。

依照这个定义,很自然能明白模板特化版本的“专业程度”比一般的版本更高,因为它有效的参数类型只有一个。
除此之外,也能自然明白:

  • T*的专用化比T更高。因为:X*类型是T模板参数的有效参数,但X不是T*模板的有效参数。
  • const TT更专业化。因为:const XT模板参数的有效参数,但X不是const T模板的有效参数。
  • const T*T*更专业化。因为:const X*T*模板参数的有效参数,但X*不是const T*模板的有效参数。
template <class T> void f(T) {cout << "普通版本" << endl;
}template <class T> void f(T*) {cout << "指针版本" << endl;
}template <class T> void f(const T*) {cout << "常量指针版本" << endl;
}int main() {int i = 0;int *pi = &i;const int *cpi = pi;f(i);   f(pi);  f(cpi); 
}

输出:

普通版本
指针版本
常量指针版本

此部分在官方文档《函数模板的部分排序 (C++) | Microsoft Docs》有更多讨论。

11. 不定数目参数

使用...可以表示不定数目(0n)的模板参数:

template<typename... Arguments> class VATClass
{
};int main() 
{VATClass< > instance1;VATClass<int> instance2;VATClass<float, bool> instance3;
}

上例可以通过编译,但没有实用,毕竟这个类的定义是空壳。官方文档提到了这个语法,但并没有展示其实际使用的场合。


我在《C++ -- variadic template (可变参数模板) - 唐风思琪 - 博客园》这篇博客中学到了更多的知识。概括来讲,是一个 “递归” 的思路,同时还要了解 “模板特化” 这个概念。
先看代码:

template<typename...Args> class VATClass;//递归关系:
template<typename LArg, typename... RArg>
class VATClass<LArg, RArg...> : public VATClass<RArg...> 
{
public:LArg data;
};//最底层的定义:
template<> 
class VATClass<>
{
};int main() 
{VATClass<int, char, double> instance;cout << sizeof(instance) << endl;
}

输出:

24

它等价于:

class EqualClass0
{
};
class EqualClass1 : public EqualClass0
{
public:double data;
};
class EqualClass2 : public EqualClass1
{
public:char data;
};
class EqualClass3 : public EqualClass2
{
public:int data;
};int main() 
{EqualClass3 instance;cout << sizeof(instance) << endl;
}

输出:

24

下面梳理一下这其中编辑器的逻辑:

  1. 首先,对于VATClass<int, char, double>:编译器找到的最“专业”的版本是:class VATClass<LArg, RArg...> : public VATClass<RArg...>。这意味着,它的父类是VATClass<char, double>
  2. 然而,对于VATClass<char, double>:编译器找到的最“专业”的版本是:class VATClass<LArg, RArg...> : public VATClass<RArg...>。这意味着,它的父类VATClass<double>
  3. 接下来,对于VATClass<double>:编译器找到的最“专业”的版本是:class VATClass<LArg, RArg...> : public VATClass<RArg...>。这意味着,它的父类是VATClass<>
  4. 最后,对于VATClass<>:编译器找到的最“专业”的版本终于变了,是特化的版本template<> class VATClass<>

12. 源代码组织

对于非模板的“类”和“函数”。通常的做法是在h文件中书写定义,然后在cpp文件中实现。然而对于模板“类”和“函数”是不行的,因为编译器在实例化模板前,不会产生任何内容。

为此,最简单、最常见的方法是将实现直接放入h文件本身。当然,这样的编译时间会较长。但也有方法减少编译时间——“显式实例化模型”,不过前提是需要明确知道将用于实例化模板的类型集。(此部分详见官方文档《源代码组织(C++ 模板) | Microsoft Docs》)

13*. 本地名称有冲突

《本地声明名称的名称解析 | Microsoft Docs》中指明了,与本地的名称冲突时的情况。

14*. 非类型模板参数的类型推导

官方文档里还说明了非类型模板参数的类型推导。但目前我还不太理解实用场合。

15*. 其他水到渠成的概念

还有一些概念,虽然官方有介绍,但是我觉得理解起来比较容易,属于“水到渠成”的概念:

  • 嵌套类模板。详见《类模板 | Microsoft Docs》
  • 模板朋友。详见《类模板 | Microsoft Docs》
  • 重用模板参数。详见《类模板 | Microsoft Docs》

总结

更系统与细节地梳理下这篇博客中讨论的问题:
在这里插入图片描述

这篇关于整理C++模板的语法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1143442

相关文章

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

c++中std::placeholders的使用方法

《c++中std::placeholders的使用方法》std::placeholders是C++标准库中的一个工具,用于在函数对象绑定时创建占位符,本文就来详细的介绍一下,具有一定的参考价值,感兴... 目录1. 基本概念2. 使用场景3. 示例示例 1:部分参数绑定示例 2:参数重排序4. 注意事项5.

使用C++将处理后的信号保存为PNG和TIFF格式

《使用C++将处理后的信号保存为PNG和TIFF格式》在信号处理领域,我们常常需要将处理结果以图像的形式保存下来,方便后续分析和展示,C++提供了多种库来处理图像数据,本文将介绍如何使用stb_ima... 目录1. PNG格式保存使用stb_imagephp_write库1.1 安装和包含库1.2 代码解

C++实现封装的顺序表的操作与实践

《C++实现封装的顺序表的操作与实践》在程序设计中,顺序表是一种常见的线性数据结构,通常用于存储具有固定顺序的元素,与链表不同,顺序表中的元素是连续存储的,因此访问速度较快,但插入和删除操作的效率可能... 目录一、顺序表的基本概念二、顺序表类的设计1. 顺序表类的成员变量2. 构造函数和析构函数三、顺序表

使用C++实现单链表的操作与实践

《使用C++实现单链表的操作与实践》在程序设计中,链表是一种常见的数据结构,特别是在动态数据管理、频繁插入和删除元素的场景中,链表相比于数组,具有更高的灵活性和高效性,尤其是在需要频繁修改数据结构的应... 目录一、单链表的基本概念二、单链表类的设计1. 节点的定义2. 链表的类定义三、单链表的操作实现四、

使用C/C++调用libcurl调试消息的方式

《使用C/C++调用libcurl调试消息的方式》在使用C/C++调用libcurl进行HTTP请求时,有时我们需要查看请求的/应答消息的内容(包括请求头和请求体)以方便调试,libcurl提供了多种... 目录1. libcurl 调试工具简介2. 输出请求消息使用 CURLOPT_VERBOSE使用 C

C++实现获取本机MAC地址与IP地址

《C++实现获取本机MAC地址与IP地址》这篇文章主要为大家详细介绍了C++实现获取本机MAC地址与IP地址的两种方式,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 实际工作中,项目上常常需要获取本机的IP地址和MAC地址,在此使用两种方案获取1.MFC中获取IP和MAC地址获取

C/C++通过IP获取局域网网卡MAC地址

《C/C++通过IP获取局域网网卡MAC地址》这篇文章主要为大家详细介绍了C++如何通过Win32API函数SendARP从IP地址获取局域网内网卡的MAC地址,感兴趣的小伙伴可以跟随小编一起学习一下... C/C++通过IP获取局域网网卡MAC地址通过win32 SendARP获取MAC地址代码#i

C++中使用vector存储并遍历数据的基本步骤

《C++中使用vector存储并遍历数据的基本步骤》C++标准模板库(STL)提供了多种容器类型,包括顺序容器、关联容器、无序关联容器和容器适配器,每种容器都有其特定的用途和特性,:本文主要介绍C... 目录(1)容器及简要描述‌php顺序容器‌‌关联容器‌‌无序关联容器‌(基于哈希表):‌容器适配器‌:(