模板 (C++)

模板是 C++ 中的泛型编程的基础。 作为强类型语言,C++ 要求所有变量都具有特定类型,由程序员显式声明或编译器推导。 但是,许多数据结构和算法无论在哪种类型上操作,看起来都是相同的。 使用模板可以定义类或函数的操作,并让用户指定这些操作应处理的具体类型。

定义和使用模板

模板是基于用户为模板参数提供的参数在编译时生成普通类型或函数的构造。 例如,可以定义如下所示的函数模板:

template <typename T>
T minimum(const T& lhs, const T& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

上面的代码描述了一个具有单个类型参数 T 的泛型函数的模板,其返回值和调用参数(lhs 和 rhs)都具有此类型。 可以随意命名类型参数,但按照约定,最常使用单个大写字母。 T 是模板参数;关键字 typename 表示此参数是类型的占位符。 调用函数时,编译器会将每个 T 实例替换为由用户指定或编译器推导的具体类型参数。 编译器从模板生成类或函数的过程称为“模板实例化”;minimum<int> 是模板 minimum<T> 的实例化。

在其他地方,用户可以声明专用于 int 的模板实例。假设 get_a() 和 get_b() 是返回 int 的函数:

int a = get_a();
int b = get_b();
int i = minimum<int>(a, b);

但是,由于这是一个函数模板,编译器可以从参数 a 和 b 中推导出类型,因此可以像普通函数一样调用它:T

int i = minimum(a, b);

当编译器遇到最后一个语句时,它会生成一个新函数,在该函数中,T 在模板中的每个匹配项都替换为 int

int minimum(const int& lhs, const int& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

编译器如何在函数模板中执行类型推导的规则基于普通函数的规则。 有关详细信息,请参阅函数模板调用的重载解析

类型参数

在上面的 minimum 模板中,请注意,在将类型参数 T 用于函数调用参数(在这些参数中会添加 const 和引用限定符)之前,不会以任何方式对其进行限定。

类型参数的数量没有实际限制。 以逗号分隔多个参数:

template <typename T, typename U, typename V> class Foo{};

在此上下文中,关键字 class 等效于 typename。 可以将前面的示例表示为:

template <class T, class U, class V> class Foo{};

可以使用省略号运算符 (...) 定义采用任意数量的零个或多个类型参数的模板:

template<typename... Arguments> class vtclass;

vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;

任何内置类型或用户定义的类型都可以用作类型参数。 例如,可以使用标准库中的 std::vector 来存储类型 intdoublestd::stringMyClassconstMyClass*、MyClass& 等的变量。 使用模板时的主要限制是类型参数必须支持应用于类型参数的任何操作。 例如,如果我们使用 MyClass 调用 minimum,如以下示例所示:

class MyClass
{
public:
    int num;
    std::wstring description;
};

int main()
{
    MyClass mc1 {1, L"hello"};
    MyClass mc2 {2, L"goodbye"};
    auto result = minimum(mc1, mc2); // Error! C2678
}

将生成编译器错误,因为 MyClass 不会为 < 运算符提供重载。

没有任何固有要求规定任何特定模板的类型参数都属于同一个对象层次结构,尽管可以定义强制实施此类限制的模板。 可以将面向对象的技巧与模板相结合;例如,可以将 Derived* 存储在向量 <Base*> 中。 请注意,自变量必须是指针

vector<MyClass*> vec;
   MyDerived d(3, L"back again", time(0));
   vec.push_back(&d);

   // or more realistically:
   vector<shared_ptr<MyClass>> vec2;
   vec2.push_back(make_shared<MyDerived>());

std::vector 和其他标准库容器对 T 的元素施加的基本要求是 T 可复制且可复制构造。

非类型参数

与其他语言(如 C# 和 Java)中的泛型类型不同,C++ 模板支持非类型参数,也称为值参数。 例如,可以提供常量整型值来指定数组的长度,例如在以下示例中,它类似于标准库中的 std::array 类:

template<typename T, size_t L>
class MyArray
{
    T arr[L];
public:
    MyArray() { ... }
};

记下模板声明中的语法。 size_t 值在编译时作为模板参数传入,必须是 constconstexpr 表达式。 可以如下所示使用它:

MyArray<MyClass*, 10> arr;

其他类型的值(包括指针和引用)可以作为非类型参数传入。 例如,可以传入指向函数或函数对象的指针,以自定义模板代码中的某些操作。

非类型模板参数的类型推导

在 Visual Studio 2017 及更高版本中,在 /std:c++17 模式或更高版本中,编译器会推导使用 auto 声明的非类型模板参数的类型:

template <auto x> constexpr auto constant = x;

auto v1 = constant<5>;      // v1 == 5, decltype(v1) is int
auto v2 = constant<true>;   // v2 == true, decltype(v2) is bool
auto v3 = constant<'a'>;    // v3 == 'a', decltype(v3) is char

模板作为模板参数

模板可以是模板参数。 在此示例中,MyClass2 有两个模板参数:类型名称参数 T 和模板参数 Arr:

template<typename T, template<typename U, int I> class Arr>
class MyClass2
{
    T t; //OK
    Arr<T, 10> a;
    U u; //Error. U not in scope
};

由于 Arr 参数本身没有正文,因此不需要其参数名称。 事实上,从 MyClass2 的正文中引用 Arr 的类型名称或类参数名称是错误的。 因此,可以省略 Arr 的类型参数名称,如以下示例所示:

template<typename T, template<typename, int> class Arr>
class MyClass2
{
    T t; //OK
    Arr<T, 10> a;
};

默认模板自变量

类和函数模板可以具有默认自变量。 如果模板具有默认自变量,可以在使用时不指定该自变量。 例如,std::vector 模板有一个用于分配器的默认自变量:

template <class T, class Allocator = allocator<T>> class vector;

在大多数情况下,默认的 std::allocator 类是可接受的,因此可以使用向量,如下所示:

vector<int> myInts;

但如有必要,可以指定自定义分配器,如下所示:

vector<int, MyAllocator> ints;

对于多个模板参数,第一个默认参数后的所有参数必须具有默认参数。

使用参数均为默认值的模板时,请使用空尖括号:

template<typename A = int, typename B = double>
class Bar
{
    //...
};
...
int main()
{
    Bar<> bar; // use all default type arguments
}

模板特殊化

在某些情况下,模板不可能或不需要为任何类型都定义完全相同的代码。 例如,你可能希望定义在类型参数为指针、std::wstring 或派生自特定基类的类型时才执行的代码路径。 在这种情况下,可以为该特定类型定义模板的专用化。 当用户使用该类型对模板进行实例化时,编译器使用该专用化来生成类,而对于所有其他类型,编译器会选择更常规的模板。 如果专用化中的所有参数都是专用的,则称为“完整专用化”。 如果只有一些参数是专用的,则称为“部分专用化”

template <typename K, typename V>
class MyMap{/*...*/};

// partial specialization for string keys
template<typename V>
class MyMap<string, V> {/*...*/};
...
MyMap<int, MyClass> classes; // uses original template
MyMap<string, MyClass> classes2; // uses the partial specialization

只要每个专用类型参数是唯一的,模板就可以具有任意数量的专用化。 只有类模板可能是部分专用。 模板的所有完整专用化和部分专用化都必须在与原始模板相同的命名空间中声明。

有关详细信息,请参阅模板规范