本文主要是介绍整理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就是一个“模板函数”,实现了一个得到二者中较小值的逻辑。
在此情境中,class
和typename
等价。因此以下的语句是等价的:
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)类型上受限制
int
、bool
、enum
经试验是可以的。
但并不是所有类型都可以使用,比如使用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 T
比T
更专业化。因为:const X
是T
模板参数的有效参数,但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. 不定数目参数
使用...
可以表示不定数目(0
到n
)的模板参数:
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
下面梳理一下这其中编辑器的逻辑:
- 首先,对于
VATClass<int, char, double>
:编译器找到的最“专业”的版本是:class VATClass<LArg, RArg...> : public VATClass<RArg...>
。这意味着,它的父类是VATClass<char, double>
。 - 然而,对于
VATClass<char, double>
:编译器找到的最“专业”的版本还是:class VATClass<LArg, RArg...> : public VATClass<RArg...>
。这意味着,它的父类VATClass<double>
。 - 接下来,对于
VATClass<double>
:编译器找到的最“专业”的版本又是:class VATClass<LArg, RArg...> : public VATClass<RArg...>
。这意味着,它的父类是VATClass<>
。 - 最后,对于
VATClass<>
:编译器找到的最“专业”的版本终于变了,是特化的版本template<> class VATClass<>
。
12. 源代码组织
对于非模板的“类”和“函数”。通常的做法是在h文件中书写定义,然后在cpp文件中实现。然而对于模板“类”和“函数”是不行的,因为编译器在实例化模板前,不会产生任何内容。
为此,最简单、最常见的方法是将实现直接放入h文件本身。当然,这样的编译时间会较长。但也有方法减少编译时间——“显式实例化模型”,不过前提是需要明确知道将用于实例化模板的类型集。(此部分详见官方文档《源代码组织(C++ 模板) | Microsoft Docs》)
13*. 本地名称有冲突
《本地声明名称的名称解析 | Microsoft Docs》中指明了,与本地的名称冲突时的情况。
14*. 非类型模板参数的类型推导
官方文档里还说明了非类型模板参数的类型推导。但目前我还不太理解实用场合。
15*. 其他水到渠成的概念
还有一些概念,虽然官方有介绍,但是我觉得理解起来比较容易,属于“水到渠成”的概念:
- 嵌套类模板。详见《类模板 | Microsoft Docs》
- 模板朋友。详见《类模板 | Microsoft Docs》
- 重用模板参数。详见《类模板 | Microsoft Docs》
总结
更系统与细节地梳理下这篇博客中讨论的问题:
这篇关于整理C++模板的语法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!