13-13 类模板
在第11.6节——函数模板中,我们介绍了这样一个难题:对于每种不同的类型组合,都需要单独创建一个(重载的)函数:
#include <iostream>
// function to calculate the greater of two int values
int max(int x, int y)
{
return (x < y) ? y : x;
}
// almost identical function to calculate the greater of two double values
// the only difference is the type information
double max(double x, double y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(5, 6); // calls max(int, int)
std::cout << '\n';
std::cout << max(1.2, 3.4); // calls max(double, double)
return 0;
}

解决此问题的方案是创建一个函数模板,编译器可利用该模板为所需的任意类型集合实例化常规函数:
#include <iostream>
// a single function template for max
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(5, 6); // instantiates and calls max<int>(int, int)
std::cout << '\n';
std::cout << max(1.2, 3.4); // instantiates and calls max<double>(double, double)
return 0;
}

相关内容:
我们在第11.7节——函数模板实例化中介绍了函数模板实例化的工作原理。
聚合类型面临类似的挑战
在处理聚合类型(包括结构体/类/联合体和数组)时,我们同样会遇到类似的挑战。
例如,假设我们正在编写一个需要处理整数对的程序,并需要判断两个数中哪个更大。我们可能会编写如下程序:
#include <iostream>
struct Pair
{
int first{};
int second{};
};
constexpr int max(Pair p) // pass by value because Pair is small
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair p1{ 5, 6 };
std::cout << max(p1) << " is larger\n";
return 0;
}

后来,我们发现还需要成对的双精度数值。因此,我们将程序更新为如下内容:
#include <iostream>
struct Pair
{
int first{};
int second{};
};
struct Pair // compile error: erroneous redefinition of Pair
{
double first{};
double second{};
};
constexpr int max(Pair p)
{
return (p.first < p.second ? p.second : p.first);
}
constexpr double max(Pair p) // compile error: overloaded function differs only by return type
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair p1{ 5, 6 };
std::cout << max(p1) << " is larger\n";
Pair p2{ 1.2, 3.4 };
std::cout << max(p2) << " is larger\n";
return 0;
}




遗憾的是,该程序无法编译,存在若干需要解决的问题。
首先,与函数不同,类型定义无法重载。编译器会将Pair的第二次定义视为对首次定义的错误重申。其次,尽管函数支持重载,但我们的max(Pair)函数仅在返回类型上存在差异,而重载函数不能仅凭返回类型进行区分。第三,这里存在大量冗余。每个Pair结构体完全相同(除数据类型外),max(Pair)函数也完全相同(除返回类型外)。
前两个问题可通过为Pair结构体命名差异化(如PairInt和PairDouble)解决。但这既需要记忆命名规则,又需为每种新增配对类型克隆大量代码,本质上未能消除冗余。
所幸我们有更优解。
作者注:
若您对函数模板、模板类型或函数模板实例化的工作原理尚不明确,请在继续学习前复习第11.6节——函数模板与第11.7节——函数模板实例化。
类模板
正如函数模板是用于实例化函数的模板定义,类模板class template则是用于实例化l类类型的模板定义。
提醒:
“类类型class type”指结构体、类或联合体类型。为简化说明,本文将通过结构体演示“类模板class templates”,但所有内容同样适用于类。
作为提醒,以下是我们 int 类型 pair 结构体的定义:
struct Pair
{
int first{};
int second{};
};
让我们将配对类重写为类模板
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
int main()
{
Pair<int> p1{ 5, 6 }; // instantiates Pair<int> and creates object p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // instantiates Pair<double> and creates object p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // creates object p3 using prior definition for Pair<double>
std::cout << p3.first << ' ' << p3.second << '\n';
return 0;
}

与函数模板类似,类模板的定义也从模板参数声明开始。首先使用模板关键字,接着在尖括号(<>)内指定类模板所需的所有模板类型。每种模板类型前需使用关键字 typename(推荐)或 class(不推荐),后跟模板类型名称(如 T)。在此示例中,由于两个成员类型相同,我们仅需定义一种模板类型。
随后按常规方式定义结构体,区别在于可将模板类型(T)用于任何需要模板类型的位置——这些类型将在后续被具体类型替换。至此,类模板定义完成。
在 main 函数中,我们可以使用任意类型实例化 Pair 对象。首先实例化 Pair
接着实例化类型为 Pair
以下是与上述相同的示例,展示所有模板实例化完成后编译器实
#include <iostream>
// A declaration for our Pair class template
// (we don't need the definition any more since it's not used)
template <typename T>
struct Pair;
// Explicitly define what Pair<int> looks like
template <> // tells the compiler this is a template type with no template parameters
struct Pair<int>
{
int first{};
int second{};
};
// Explicitly define what Pair<double> looks like
template <> // tells the compiler this is a template type with no template parameters
struct Pair<double>
{
double first{};
double second{};
};
int main()
{
Pair<int> p1{ 5, 6 }; // instantiates Pair<int> and creates object p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // instantiates Pair<double> and creates object p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // creates object p3 using prior definition for Pair<double>
std::cout << p3.first << ' ' << p3.second << '\n';
return 0;
}

你可以直接编译这个示例,并看到它按预期运行!
面向高级读者:
上述示例运用了名为类模板特化的特性(将在后续第26.4节——类模板特化中讲解)。目前无需了解该特性的具体工作原理。
在函数中使用类模板
现在让我们回到让 max() 函数支持不同类型的挑战。由于编译器将 Pair
constexpr int max(Pair<int> p)
{
return (p.first < p.second ? p.second : p.first);
}
constexpr double max(Pair<double> p) // okay: overloaded function differentiated by parameter type
{
return (p.first < p.second ? p.second : p.first);
}
虽然这段代码能编译通过,但并未解决冗余问题。我们真正需要的是能接受任意类型元组的函数。换言之,我们需要一个接受类型为 Pair
以下是完整示例,其中 max() 被实现为函数模板:
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
template <typename T>
constexpr T max(Pair<T> p)
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair<int> p1{ 5, 6 };
std::cout << max<int>(p1) << " is larger\n"; // explicit call to max<int>
Pair<double> p2{ 1.2, 3.4 };
std::cout << max(p2) << " is larger\n"; // call to max<double> using template argument deduction (prefer this)
return 0;
}

max()函数模板的设计相当直观。由于需要传入Pair
当 max() 函数被 Pair
template <>
constexpr int max(Pair<int> p)
{
return (p.first < p.second ? p.second : p.first);
}
与所有函数模板调用相同,我们可以显式指定模板类型参数(例如 max
包含模板类型成员和非模板类型成员的类模板
类模板可以包含使用模板类型的成员,同时也包含使用普通(非模板)类型的成员。例如:
template <typename T>
struct Foo
{
T first{}; // first will have whatever type T is replaced with
int second{}; // second will always have type int, regardless of what type T is
};
这完全符合预期:第一个参数将是模板类型 T 的具体类型,第二个参数始终是 int 类型。
具有多种模板类型的类模板
类模板也可以包含多种模板类型。例如,若希望Pair类的两个成员能够具有不同类型,我们可以为Pair类模板定义两种模板类型:
#include <iostream>
template <typename T, typename U>
struct Pair
{
T first{};
U second{};
};
template <typename T, typename U>
void print(Pair<T, U> p)
{
std::cout << '[' << p.first << ", " << p.second << ']';
}
int main()
{
Pair<int, double> p1{ 1, 2.3 }; // a pair holding an int and a double
Pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int
Pair<int, int> p3{ 7, 8 }; // a pair holding two ints
print(p2);
return 0;
}

要定义多个模板类型,在模板参数声明中,我们用逗号分隔每个所需的模板类型。在上面的示例中,我们定义了两种不同的模板类型,一种名为 T,一种名为 U。T 和 U 的实际模板类型参数可以不同(如上面的 p1 和 p2 的情况)或相同(如 p3 的情况)。
使函数模板支持多种类类型
考虑上述示例中的 print() 函数模板:
template <typename T, typename U>
void print(Pair<T, U> p)
{
std::cout << '[' << p.first << ", " << p.second << ']';
}
由于我们已将函数参数显式定义为 Pair<T, U>,因此只有类型为 Pair<T, U> 的参数(或可转换为 Pair<T, U> 的参数)才会匹配。若我们仅希望函数能接受 Pair<T, U> 参数调用,这种设计最为理想。
在某些情况下,我们可能需要编写适用于任何类型且能成功编译的函数模板。此时只需将类型模板参数作为函数参数即可实现。
例如:
#include <iostream>
template <typename T, typename U>
struct Pair
{
T first{};
U second{};
};
struct Point
{
int first{};
int second{};
};
template <typename T>
void print(T p) // type template parameter will match anything
{
std::cout << '[' << p.first << ", " << p.second << ']'; // will only compile if type has first and second members
}
int main()
{
Pair<double, int> p1{ 4.5, 6 };
print(p1); // matches print(Pair<double, int>)
std::cout << '\n';
Point p2 { 7, 8 };
print(p2); // matches print(Point)
std::cout << '\n';
return 0;
}

在上例中,我们重写了print()函数,使其仅包含单个类型模板参数(T),该参数可匹配任意类型。只要对象具有第一个和第二个成员,函数主体就能成功编译。我们通过调用print()函数来演示这一点:先传入类型为Pair<double, int>的对象,再传入类型为Point的对象。
存在一种可能产生误解的情况。请考虑以下版本的print():
template <typename T, typename U>
struct Pair // defines a class type named Pair
{
T first{};
U second{};
};
template <typename Pair> // defines a type template parameter named Pair (shadows Pair class type)
void print(Pair p) // this refers to template parameter Pair, not class type Pair
{
std::cout << '[' << p.first << ", " << p.second << ']';
}
你可能会认为该函数仅在传入Pair类类型参数时才会匹配。但此版本的print()在功能上与先前将模板参数命名为T的版本完全相同,可匹配任何类型。问题在于当我们将Pair定义为类型模板参数时,它会遮蔽全局作用域内其他对Pair名称的使用。因此在函数模板内部,Pair指代的是模板参数Pair,而非类类型Pair。由于类型模板参数会匹配任何类型,这个Pair便能匹配任意参数类型,而不仅限于类类型Pair!
这正是坚持使用简单模板参数名称(如T、U、N)的合理依据——这类名称较少会遮蔽类类型名称。
std::pair
由于成对数据的操作十分常见,C++标准库中包含一个名为std::pair的类模板(位于
#include <iostream>
#include <utility>
template <typename T, typename U>
void print(std::pair<T, U> p)
{
// the members of std::pair have predefined names `first` and `second`
std::cout << '[' << p.first << ", " << p.second << ']';
}
int main()
{
std::pair<int, double> p1{ 1, 2.3 }; // a pair holding an int and a double
std::pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int
std::pair<int, int> p3{ 7, 8 }; // a pair holding two ints
print(p2);
return 0;
}

在本节课中,我们开发了自己的Pair类来演示其工作原理,但在实际编码中,应优先使用std::pair而非自行编写。
在多个文件中使用类模板
与函数模板类似,类模板通常定义在头文件中,以便被任何需要它们的代码文件包含。模板定义和类型定义均不受单定义规则限制,因此不会引发问题:
pair.h
#ifndef PAIR_H
#define PAIR_H
template <typename T>
struct Pair
{
T first{};
T second{};
};
template <typename T>
constexpr T max(Pair<T> p)
{
return (p.first < p.second ? p.second : p.first);
}
#endif
foo.cpp:
#include "pair.h"
#include <iostream>
void foo()
{
Pair<int> p1{ 1, 2 };
std::cout << max(p1) << " is larger\n";
}
main.cpp:
#include "pair.h"
#include <iostream>
void foo(); // forward declaration for function foo()
int main()
{
Pair<double> p2 { 3.4, 5.6 };
std::cout << max(p2) << " is larger\n";
foo();
return 0;
}


浙公网安备 33010602011771号