整理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

相关文章

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

Java使用ANTLR4对Lua脚本语法校验详解

《Java使用ANTLR4对Lua脚本语法校验详解》ANTLR是一个强大的解析器生成器,用于读取、处理、执行或翻译结构化文本或二进制文件,下面就跟随小编一起看看Java如何使用ANTLR4对Lua脚本... 目录什么是ANTLR?第一个例子ANTLR4 的工作流程Lua脚本语法校验准备一个Lua Gramm

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字

IDEA自动生成注释模板的配置教程

《IDEA自动生成注释模板的配置教程》本文介绍了如何在IntelliJIDEA中配置类和方法的注释模板,包括自动生成项目名称、包名、日期和时间等内容,以及如何定制参数和返回值的注释格式,需要的朋友可以... 目录项目场景配置方法类注释模板定义类开头的注释步骤类注释效果方法注释模板定义方法开头的注释步骤方法注

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

C++ vector的常见用法超详细讲解

《C++vector的常见用法超详细讲解》:本文主要介绍C++vector的常见用法,包括C++中vector容器的定义、初始化方法、访问元素、常用函数及其时间复杂度,通过代码介绍的非常详细,... 目录1、vector的定义2、vector常用初始化方法1、使编程用花括号直接赋值2、使用圆括号赋值3、ve

如何高效移除C++关联容器中的元素

《如何高效移除C++关联容器中的元素》关联容器和顺序容器有着很大不同,关联容器中的元素是按照关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,本文介绍了如何高效移除C+... 目录一、简介二、移除给定位置的元素三、移除与特定键值等价的元素四、移除满足特android定条件的元

Python基础语法中defaultdict的使用小结

《Python基础语法中defaultdict的使用小结》Python的defaultdict是collections模块中提供的一种特殊的字典类型,它与普通的字典(dict)有着相似的功能,本文主要... 目录示例1示例2python的defaultdict是collections模块中提供的一种特殊的字

Python获取C++中返回的char*字段的两种思路

《Python获取C++中返回的char*字段的两种思路》有时候需要获取C++函数中返回来的不定长的char*字符串,本文小编为大家找到了两种解决问题的思路,感兴趣的小伙伴可以跟随小编一起学习一下... 有时候需要获取C++函数中返回来的不定长的char*字符串,目前我找到两种解决问题的思路,具体实现如下: