整理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++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

poj3468(线段树成段更新模板题)

题意:包括两个操作:1、将[a.b]上的数字加上v;2、查询区间[a,b]上的和 下面的介绍是下解题思路: 首先介绍  lazy-tag思想:用一个变量记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率。 比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果刚好执行到一个子节点,

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题:

uva 1342 欧拉定理(计算几何模板)

题意: 给几个点,把这几个点用直线连起来,求这些直线把平面分成了几个。 解析: 欧拉定理: 顶点数 + 面数 - 边数= 2。 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <cmath>#inc

uva 11178 计算集合模板题

题意: 求三角形行三个角三等分点射线交出的内三角形坐标。 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <cmath>#include <stack>#include <vector>#include <