目录
面向对象编程(OOP)和泛型编程都能处理在编写程序时不知类型的情况,不同之处在于:OOP能处理类型在程序运行之前都未知的情况:而在泛型编程中,在编译时就能获知类型了
1. 定义模板
1.1. 函数模板
背景:比如我们需要来比较不同数据类型的两个值,并指出第一个值与第二个值的大小关系,如果此时只定义只比较一种参数类型的函数,略显麻烦。
此时引出了函数模板,此时就不用为每个类型定义一个新的函数了。
比如:
template<typename T>
int compare(const T& v1, const T& v2)
{
if (v1 < v2)return -1;
if (v2 < v1)return 1;
return 0;
}
模版定义:以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用小于号(<)和大于号(>)包围起来。
实例化函数模板
当我们调用一个函数模版时,编译器通常根据函数传入的实参来推断模板实参
例如,
cout << compare(1, 0) << endl;//T为int
实参为int型,编译器会将模板实参推断为int,并将它绑定到模板参数T
- 编译器用推断出的模板参数来为我们实例化,这些编译器生成的版本通常被称为模板的实例
模板类型参数
在模板参数列表中,使用关键字typename
或class
后面跟着一个标识符来定义模板类型参数。例如:
template<typename T>
void print(T value) {
std::cout << value << std::endl;
}
在上面的例子中,T
是一个模板类型参数,它表示一个不特定的数据类型。在函数体内,我们可以像使用任何其他类型一样使用T
。
- 返回类型和参数类型相同的模板示例:
template<typename T>
T identity(T value) {
return value;
}
在上面的例子
identity
是一个模板函数,它接受一个参数value
,类型为T
,并返回相同类型的值。无论传入什么类型的参数,该函数都会返回相同类型的值。
使用该模板函数的示例代码如下:
int main() {
int intValue = identity(42); // 返回 int 类型的值 42
double doubleValue = identity(3.14); // 返回 double 类型的值 3.14
std::string stringValue = identity("Hello"); // 返回 std::string 类型的值 "Hello"
return 0;
}
在上述示例中,
identity
函数根据传入的参数类型进行实例化,并返回相同类型的值。模板类型参数前必须使用关键字
class
或typename
,这是为了指示编译器该参数是一个类型参数。选择使用哪个关键字主要是个人偏好,但在C++标准中,更常见的做法是使用关键字 typename 来表示类型参数
非类型模板参数
非类型模板参数(Non-type Template Parameters):非类型模板参数允许我们在模板中使用常量值作为参数。它们用于在模板定义中指定一个常量值,而不是一个数据类型。非类型模板参数可以是整数、枚举、指针或引用类型。在模板参数列表中,我们使用一个特定的类型来定义非类型模板参数。例如:
template<int N>
int multiplyByN(int value) {
return value * N;
}
在上面的例子中,
N
是一个非类型模板参数,它表示一个整数常量值。在函数体内,我们可以将N
用作常量值来执行相应的计算。
- 非类型模版参数使用示例:
模版定义了两个非类型的参数。第一个模板参数表示第一个数组的长度,第二个模版参数表示第二个数组的长度:
template<unsigned N,unsigned M>
int compare(const char(&p1)[N], const char(&p2)[M])
//由于数组不能拷贝,所以定义为数组的引用
{
return strcmp(p1, p2);
}
当我们这样调用时:
compare("hi","mom");
编译器会使用字面常量的大小来代替N和M,从而实例化模版:
int compare(const char (&p1)[3],const char (&p2)[4])
- 一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用
- 绑定到非类型整型参数的实参必须是一个常量表达式
- 在模版定义内,模版非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数。例如指定数组大小
inline 和 constexpr的函数模版
函数模版可以声明为inline或constexpr的,如同非模版函数一样。inline或constexpr说明符放在模版参数列表之后,返回类型之前:
//正确:inline说明符跟在模版参数列表之后
template <typename T> inline T min(const T&,const T&);
//错误:inline 说明符的位置不正确
inline template<typename T> T min(const T&,const T&);
编写类型无关的代码
- 将参数设定为const的引用可以避免不必要的拷贝操作
这个原则确保了函数可以用于不能拷贝的类型,因为将参数设定为const的引用可以避免不必要的拷贝操作。这使得函数可以处理不允许拷贝的类类型,并且如果函数用于处理大对象,这种设计策略还能使函数运行得更快。
- 函数体中的条件判断仅使用<比较运算:
意味着在函数体内部,比较操作只使用小于号(<)进行判断,而不使用其他比较操作符,比如大于号(>)或等于号(==)。
举个例子,假设我们有一个通用的比较函数模板:
template <typename T>
int compare(const T &v1, const T &v2) {
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
在这个例子中,我们只使用小于号(<)进行比较判断。如果v1小于v2,我们返回-1;如果v2小于v1,我们返回1;如果它们相等,我们返回0。这样的设计保证了我们只依赖于小于号来进行比较,而不会依赖于其他比较操作符。
这种方法的好处在于,它降低了函数对于要处理的类型的要求。被比较的类型只需要支持小于号(<)运算符,而不需要同时支持其他比较操作符。
模版编译
模板编译指的是使用C++中的模板来生成特定类型的代码实例。在C++中,当使用模板时,编译器并不直接生成代码,而是在实际使用时根据模板参数生成对应的代码。
函数模版和类模版成员函数的定义通常放在头文件中
1.2. 类模版
类似函数模版,类模版以关键字template开始,后跟模版参数列表。
定义类模版
template <typename T1, typename T2>
class Pair {
private:
T1 first;
T2 second;
public:
Pair(const T1& f, const T2& s) : first(f), second(s) {}
T1 getFirst() const { return first; }
T2 getSecond() const { return second; }
};
实例化类模版
- 实例化了一个
Pair
对象,其中T1
被实例化为int
,T2
被实例化为double
:
Pair<int, double> myPair(5, 3.14);
- 使用显式模版实参:
作用:使用显式模板实参列表来明确指定模板实例化时使用的模板参数,而不是让编译器根据上下文推断,这在某些情况下可能是必要的。
格式:当使用显式模板实参列表时,可以在模板名称后面使用尖括号(<>)来指定模板参数。这样可以确保使用指定的模板参数进行实例化,而不依赖于编译器的模板参数推断。
#include <iostream>
template <typename T>
void printValue(T value) {
std::cout << value << std::endl;
}
int main() {
printValue<int>(5); // 显式指定模板参数为int
// printValue(5); //不显式指定模版参数,编译器会推断模板参数为int
printValue<double>(3.14); // 显式指定模板参数为double
printValue(3.14); // 不显式指定模版参数,编译器会推断模板参数为double
return 0;
}
<int>
和 <double>
就是显式模板实参列表,它们明确指定了在调用printValue
函数时应该使用的模板参数类型。
类模版的成员函数
- 定义在类模板之外的成员函数
在类模板之外定义成员函数时,需要显式指定模板参数,这样才能让编译器知道这个函数是属于哪个类模板的。这种方式使得你可以单独实现类模板的成员函数,而不必将实现和类模板的定义放在一起。
下面是一个示例代码:
template <typename T>
class MyContainer {
private:
T element;
public:
MyContainer(const T& elem) : element(elem) {}
T getElement() const;
};
// 在类模板之外定义成员函数
template <typename T>
T MyContainer<T>::getElement() const {
return element;
}
- 在类模版外定义成员函数时不指定模版参数的错误示例:
// 在类模板之外错误地未指定模板参数
// 这将导致编译错误
template
T MyContainer::getElement() const {
return element;
}
- 定义在类模板之内的成员函数
如果选择在类模板内部定义成员函数,可以直接访问类模板的成员和类型,而无需显式指定模板参数。这种方式更为简洁,因为函数的定义直接位于类模板内部。
下面是一个示例代码:
template <typename T>
class MyContainer {
private:
T element;
public:
MyContainer(const T& elem) : element(elem) {}
T getElement() const {
return element;
}
};
类模版构造函数
在类模板内部定义构造函数时,不需要显式指定模板参数,因为在类模板内部,构造函数的定义已经在模板的作用域内,编译器可以自动识别模板参数:
template<typename T>
class MyContainer {
private:
T element;
public:
MyContainer(const T& element) :element(elem) {
}
};
在类模板外部定义构造函数,需要使用与类模板的成员函数类似的语法来指定模板参数:
template<typename T>
class MyContainer {
private:
T element;
public:
MyContainer(const T& element);
};
template<typename T>
MyContainer<T>::MyContainer(const T& element:element(elem) {
}
在类模版外使用类模版名
// 声明类模板
template <typename T>
class MyTemplate {
public:
void doSomething(const T& value); // 成员函数声明
};
// 类模版外定义成员函数
template <typename T>
void MyTemplate<T>::doSomething(const T& value) {
// 在类模板外部使用类模板名 MyTemplate<T>
// 实现类模板的成员函数
// 这里可以使用模板参数 T
}
在类模版中的友元
1. 声明友元函数模板
#include <iostream>
// 定义一个类模板 MyClass
template <typename T>
class MyClass {
public:
// 构造函数,初始化成员变量 value
MyClass(T value) : value(value) {}
// 声明一个友元函数模板
// 这个友元函数模板可以访问 MyClass 的私有成员
template <typename U>
friend void printValue(const MyClass<U>& obj);
private:
// 私有成员变量
T value;
};
// 定义友元函数模板
// 这个函数可以访问 MyClass 的私有成员
template <typename U>
void printValue(const MyClass<U>& obj) {
std::cout << "Value: " << obj.value << std::endl;
}
int main() {
// 创建 MyClass<int> 的实例
MyClass<int> obj(42);
// 调用友元函数模板,输出成员变量 value 的值
printValue(obj); // 输出: Value: 42
return 0;
}
2. 声明友元类模板
#include <iostream>
// 前向声明 MyClass 类模板
template <typename T>
class MyClass;
// 定义 MyClass 类模板
template <typename T>
class MyClass {
public:
// 构造函数,初始化成员变量 value
MyClass(T value) : value(value) {}
// 声明 FriendClass<T> 为友元类
// FriendClass<T> 可以访问 MyClass<T> 的私有成员
friend class FriendClass<T>;
private:
// 私有成员变量
T value;
};
// 定义一个友元类模板 FriendClass
template <typename T>
class FriendClass {
public:
// 定义一个成员函数,可以访问 MyClass 的私有成员
void showValue(const MyClass<T>& obj);
};
// 定义 FriendClass<T> 的成员函数
// 这个函数可以访问 MyClass<T> 的私有成员
template <typename T>
void FriendClass<T>::showValue(const MyClass<T>& obj) {
std::cout << "Value: " << obj.value << std::endl;
}
int main() {
// 创建 MyClass<int> 的实例
MyClass<int> obj(42);
// 创建 FriendClass<int> 的实例
FriendClass<int> friendObj;
// 调用 FriendClass<int> 的成员函数,输出 MyClass<int> 的成员变量 value 的值
friendObj.showValue(obj); // 输出: Value: 42
return 0;
}
3. 声明特定实例的友元函数
#include <iostream>
// 定义一个类模板 MyClass
template <typename T>
class MyClass {
public:
// 构造函数,初始化成员变量 value
MyClass(T value) : value(value) {}
// 声明一个特定实例的友元函数
// 这个友元函数可以访问 MyClass<T> 的私有成员
friend void printValue(const MyClass<T>& obj) {
std::cout << "Value: " << obj.value << std::endl;
}
private:
// 私有成员变量
T value;
};
int main() {
// 创建 MyClass<int> 的实例
MyClass<int> obj(42);
// 调用友元函数,输出成员变量 value 的值
printValue(obj); // 输出: Value: 42
return 0;
}
类模版的static成员
C++中的类模板可以声明静态成员。静态成员变量和静态成员函数在类模板中的使用方式与非模板类类似。静态成员变量只有一个实例,无论模板实例化了多少次,而静态成员函数在类的所有实例之间共享。
1.3. 模版参数
我们通常将类型参数命名为T,但实际上我们可以使用任何名字:
template<typename Foo>Foo calc(const Foo& a, const Foo& b)
{
Foo tmp = a;//tmp的类型与参数和返回类型一样
//..
return tmp;//返回类型和参数类型一样
}
模版声明
- 模版声明必须包含模版参数:
template <typename T> T calc(const T&, const T&);//声明
- 与函数参数相同,模版定义中模版参数的名字不必与声明中相同:
template<typename Type>
Type calc(const Type& a, const Type& b) {/**/ }//定义
可以分成两行写,也可以都写在一行内
使用typename来区分成员和类型
- 在普通(非模板)代码中,编译器能够通过访问类定义来确定通过作用域运算符访问的名称是类型还是静态成员。
- 在处理模板类型参数(在本例中表示为T)时,编译器在实例化之前无法确定通过作用域运算符访问的名称是类型还是静态数据成员。
- 为了处理模板,编译器必须知道一个名称是否表示一个类型。例如,当遇到类似T::size_type * p;的语句时,编译器需要知道size_type是类型还是静态数据成员,以便正确解释该语句。
- 默认情况下,在模板的上下文中,语言假定通过作用域运算符访问的名称不是类型。这意味着,如果要使用模板类型参数的类型成员,必须明确告诉编译器该名称是一个类型。
typename T::size_type * p;
- 当我们希望通知编译器一个名字表示类型时,必须使用关键字
typename
,而不能使用class
默认模板实参
我们也可以向能为函数参数提供默认实参一样,我们也可以提供默认模版实参
template <typename T, typename F = std::less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
if (f(v1, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
compare
函数是一个模板函数,它接受三个参数,但是只有前两个参数是必须的。第三个参数是一个可选的函数对象参数,它有一个默认值。
在调用 compare
函数时,如果只提供了前两个参数 v1
和 v2
,编译器会根据函数模板参数的默认类型进行自动推导。这意味着它会使用 less<T>
作为默认的函数对象类型。因此,下面这两种调用方式都是合法的:
int result1 = compare(5, 10); // 使用默认的比较函数对象 less<int>
int result2 = compare(5, 10, MyComparator()); // 使用自定义的比较函数对象 MyComparator
在第一种调用中,由于没有提供第三个参数,编译器会使用默认的函数对象 less<int>
进行比较。在第二种调用中,我们显式地提供了自定义的函数对象 MyComparator
,因此它会被用于执行比较操作。
函数对象是一个行为类似函数的对象,它可以被调用并执行特定的操作。在C++中,函数对象通常是一个类对象,它重载了函数调用运算符
operator()
。函数对象在泛型编程中非常有用,因为它们可以作为参数传递给函数或算法,从而实现定制化的行为。举例来说,假设有一个自定义的比较函数对象
MyComparator
,它实现了一个特定的比较操作。用户可以将这个函数对象传递给compare
函数,以便在比较时使用这个自定义的比较行为,如以下示例:
struct MyComparator {
bool operator()(int a, int b) const {
return a < b; // 自定义的比较操作
}
};
int main() {
int result = compare(5, 10, MyComparator());
// 使用自定义的比较函数对象进行比较
}
在这个例子中,
MyComparator
是一个函数对象,它重载了函数调用运算符,因此可以被当作一个函数来调用。当它被传递给compare
函数时,它将会被用于执行比较操作。
模板默认实参与类模板
无论任何时候使用类模版,都必须在模版名后接上一个尖括号。尖括号指出类必须从一个模板实例化而来。如果类模板为所有的模板参数提供了默认实参:
template <typename T = int, int N = 5>
class MyClassTemplate {
// ... class definition
};
那么在调用时候,使用<>表示我们希望使用默认实参:
MyClassTemplate<> obj1; // Using default template arguments for T and N
1.4. 成员模板
无论是普通类还是类模板,都可以包含一个本身是模板的成员函数。这种成员被称为成员模板。
【成员模板不能是虚函数】
非模板类的成员模板
#include <iostream>
// 普通类
class MyClass {
public:
// 成员模板
template <typename T>
void print(const T& value) {
std::cout << "Value: " << value << std::endl;
}
};
int main() {
MyClass obj;
obj.print(5); // 调用模板成员函数,T 被推导为 int
obj.print("Hello"); // 调用模板成员函数,T 被推导为 const char*
return 0;
}
类模板的成员模板
假设我们有一个类模板 MyClass
,它有一个成员模板 func
。我们需要在类模板外定义这个成员模板 func
。为了做到这一点,必须同时为类模板和成员模板提供参数列表。
首先,定义类模板 MyClass
:
#include <iostream>
template <typename T>
class MyClass {
public:
// 成员模板
template <typename U>
void func(U value);
};
// 在类模板外定义成员模板
template <typename T>
template <typename U>
void MyClass<T>::func(U value) {
std::cout << "T: " << typeid(T).name() << ", U: " << typeid(U).name() << ", value: " << value << std::endl;
}
int main() {
MyClass<int> myClass;
myClass.func(3.14); // 调用 func 成员模板,U 被推断为 double
myClass.func("Hello"); // 调用 func 成员模板,U 被推断为 const char*
return 0;
}
在这个例子中:
- 定义了类模板
MyClass
,它有一个模板参数T
。- 在
MyClass
中,定义了一个成员模板func
,它有自己的模板参数U
。- 在类模板外定义成员模板时,必须同时为类模板和成员模板提供参数列表。
template <typename T>
是类模板MyClass
的参数列表。template <typename U>
是成员模板func
的参数列表。- 在
main
函数中,创建了一个MyClass<int>
的实例,并调用了func
成员模板。
实例化与成员模板
必须同时提供类和函数模板的实参,才能实例化一个类模板的成员模板:
template <typename T>
class MyClass {
public:
template <typename U>
void MyFunction(U value) {
// 实现代码
}
};
int main() {
MyClass<int> obj; // 实例化类模板 MyClass
obj.MyFunction(5.5); // 这里需要提供函数模板 MyFunction 的实参
return 0;
}
可以在实例化类模板后,使用不同的类型来调用类模板的成员函数模板,比如这里使用double
类型来实例化函数模板,而使用int
型来实例化类模板
1.5. 继承关系
父类是一般类,子类是模板类
这种情况下,父类是一个普通的非模板类,而子类是一个模板类。子类可以通过模板参数来扩展父类的功能:
class Base {
public:
void baseFunction() {
// 基类的方法
}
};
template <typename T>
class Derived : public Base {
public:
void derivedFunction() {
// 子类的方法
}
};
用法示例:
Derived<int> d;
d.baseFunction(); // 调用基类的方法
d.derivedFunction(); // 调用子类的方法
父类是模板类,子类是普通类
这种情况下,父类是一个模板类,而子类是一个普通类。子类需要在定义时明确父类的模板参数:
template <typename T>
class Base {
public:
void baseFunction() {
// 基类的方法
}
};
class Derived : public Base<int> {
public:
void derivedFunction() {
// 子类的方法
}
};
用法示例:
Derived d;
d.baseFunction(); // 调用基类的方法
d.derivedFunction(); // 调用子类的方法
父类和子类都是模板类
这种情况下,父类和子类都是模板类。子类可以继承父类并扩展其功能,且可以有不同的模板参数:
template <typename T>
class Base {
public:
void baseFunction() {
// 基类的方法
}
};
template <typename T, typename U>
class Derived : public Base<T> {
public:
void derivedFunction() {
// 子类的方法
}
};
用法示例:
Derived<int, double> d;
d.baseFunction(); // 调用基类的方法
d.derivedFunction(); // 调用子类的方法
注意事项
- 访问基类成员:在模板类中访问基类成员时,有时需要使用
this->
或Base<T>::
来明确指示编译器成员是来自基类的。 - 模板参数的传递:在继承模板类时,子类需要明确父类的模板参数,或者将父类的模板参数传递给子类。
- 模板类的实例化:模板类在使用时需要提供具体的模板参数,否则编译器无法生成对应的类定义。