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

相关文章

MySQL 多列 IN 查询之语法、性能与实战技巧(最新整理)

《MySQL多列IN查询之语法、性能与实战技巧(最新整理)》本文详解MySQL多列IN查询,对比传统OR写法,强调其简洁高效,适合批量匹配复合键,通过联合索引、分批次优化提升性能,兼容多种数据库... 目录一、基础语法:多列 IN 的两种写法1. 直接值列表2. 子查询二、对比传统 OR 的写法三、性能分析

c++ 类成员变量默认初始值的实现

《c++类成员变量默认初始值的实现》本文主要介绍了c++类成员变量默认初始值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录C++类成员变量初始化c++类的变量的初始化在C++中,如果使用类成员变量时未给定其初始值,那么它将被

Javaee多线程之进程和线程之间的区别和联系(最新整理)

《Javaee多线程之进程和线程之间的区别和联系(最新整理)》进程是资源分配单位,线程是调度执行单位,共享资源更高效,创建线程五种方式:继承Thread、Runnable接口、匿名类、lambda,r... 目录进程和线程进程线程进程和线程的区别创建线程的五种写法继承Thread,重写run实现Runnab

C++中NULL与nullptr的区别小结

《C++中NULL与nullptr的区别小结》本文介绍了C++编程中NULL与nullptr的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编... 目录C++98空值——NULLC++11空值——nullptr区别对比示例 C++98空值——NUL

C++ Log4cpp跨平台日志库的使用小结

《C++Log4cpp跨平台日志库的使用小结》Log4cpp是c++类库,本文详细介绍了C++日志库log4cpp的使用方法,及设置日志输出格式和优先级,具有一定的参考价值,感兴趣的可以了解一下... 目录一、介绍1. log4cpp的日志方式2.设置日志输出的格式3. 设置日志的输出优先级二、Window

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注

MySQL 删除数据详解(最新整理)

《MySQL删除数据详解(最新整理)》:本文主要介绍MySQL删除数据的相关知识,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、前言二、mysql 中的三种删除方式1.DELETE语句✅ 基本语法: 示例:2.TRUNCATE语句✅ 基本语

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性: