Chapter 28: Templates

模板提供了一种方法使得类,函数或变量处理不同的数据类型,而无需为每种类型重写代码。

函数模板

下面的示例展示了交换两个整数参数的函数。

void swap(int& a, int& b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

要将此方法转换为能够处理任意类型的函数模板,第一步是在函数前添加模板参数声明。此声明包含template关键字后跟关键字typename以及模板类型参数名,两者均包含在尖括号中。模板参数名可以是任意名字,但通常将其命名为大写T。

template<typename T>

或者可以使用关键字class替换typename。在此语境下它们等同。

template<class T>

创建函数模板的第二步是将数据类型替换为模板类型参数。

template<class T>
void swap(T& a, T& b)
{
    T tmp = a;
    a = b;
    b = tmp;
}

函数模板现在已完成。要使用模板函数,你可以像调用普通函数一样调用swap,但在需要在函数名前的尖括号中指定所需的模板参数。在幕后,编译器将会使用所填充的模板参数实例化一个新函数,而在此行所调用的是生成的函数。

int a = 1, b = 2;
swap<int>(a,b); // calls int version of swap

每次使用新类型调用模板函数时,编译器将会使用模板实例化一个新函数。

bool c = true, d = false;
swap<bool>(c,d); // calls bool version of swap

在此示例中,也可以不指定模板参数调用swap函数模板。这是因为编译器能够自动确定类型,因为函数模板的参数使用模板类型。然而,如果不是此类情况,或者需要强制编译器来选择特定的函数模板实例,则需要在括号中显式指定模板参数。

int e = 1, f = 2;
swap(e,f); // calls int version of swap

多模板参数

通过将模板参数添加到括号中并以逗号分隔,模板也可以定义为接受多个模板参数。

template<class T, class U>
void swap(T& a, U& b)
{
    T tmp = a;
    a = b;
    b = tmp;
}

此示例中的第二个模板参数允许swap使用两个不同类型的参数进行调用。

int main()
{
    int a = 1;
    long b = 2;
    swap<int, long>(a,b);
    swap(a,b); // alternative
}

类模板

类模板允许类成员使用模板参数作为类型。它们使用与函数模板相同的方式进行创建。

template<class T>
class MyBox
{
public:
    T a, b;
    MyBox(const T& x, const T& y) : a(x), b(y) {}
};

基于传递给类构造器的参数,编译器可以推断模板类型参数。

int main()
{
    // Without type deduction
    MyBox<int> box(1, 2); // MyBox<int>
    // With type deduction
    MyBox box(2.1, 3.2); // MyBox<double>
}

当使用类模板时需要记住的另一点是,如果一个方法在类模板的外部定义,此定义必须以模板声明作为前缀。

template<class T>
class MyBox
{
public:
    T a, b;
    void swap();
};

void MyBox<T>::swap()
{
    T tmp = a;
    a = b;
    b = tmp;
}

注意,模板参数包含在类名限定符后的swap模板函数定义中。这指定了函数的模板参数与类的模板参数相同。

非类型参数

在类参数之外,类与函数模板还可以拥有普通的函数类参数。例如,unsigned int模板参数用来指定数组的大小。

template<class T, unsigned int N>
class MyBox
{
public:
    T store[N];
}

当此类模板被实例化时,需要同时包含类型与整数。

MyBox<int, 5> box;

默认类型与值

类与函数模板参数可以指定默认值与类型。

template<class T = int, int N = 5>

要使用这些默认值,当实例化类模板时,只需要将尖括号留空。

MyBox<> box;

类模板特化

当传递特定的类型作为模板参数时,如果需要为模板定义一个不同的实现,可以声明一个模板特化。例如,在下面的类模板中,有一个输出类模板域的值的print方法。

#include <iostream>
template<class T>
class MyBox
{
public:
    T a;
    void print() { std::cout << a; }
};

当模板参数为bool时,方法应输出true或false,而不是1或0。一种实现方法是创建一个类模板特化。需要创建类模板的一个重新实现,其中模板参数列表为空。相反,bool特化参数被放置于类模板名字之后,则在实现中使用此数据类型,而不是模板参数。

template<>
class MyBox<bool>
{
public:
    bool a;
    void print() { std::cout << (a ? "true" : "false"); }
};

当此类模板使用bool作为模板类型实例化时,则会使用此模板特化,而是标准类。

int main()
{
    MyBox<bool> box { true };
    box.print(); // "true"
}

注意,由标准模板到特化模板并没有任何成员继承。整个类需要重新定义。

函数模板特化

在前面的示例中,由于模板之间的区别仅在于一个函数的区别,因而一种更好的方法是创建一个函数模板特化。这种特化类似于类模板特化,但仅应用于单个函数而不是整个类。

#include <iostream>
template<class T>
class MyBox
{
public:
    T a;
    template<class T> void print() {
        std::cout << a;
    }
    template<> void print<bool>() {
        std::cout << (a ? "true" : "false");
    }
};

这样,仅print方法,而不是整个类需要被重新定义。

int main()
{
    MyBox<bool> box = { true };
    box.print<bool>(); // "true"
}

注意,当特化函数被调用时,需要指定模板参数。这不同于类模板特化的情况。

变量模板

在函数模板与类模板之外,C++14允许变量模板化。这是使用普通的模板语法来实现的。

template<class T>
constexpr T pi = T(3.1415926535897932384626433L);

配合constexpr修饰符,模板允许变量的值在编译时为指定的类型进行计算,而无需类型转换。

int i = pi<int>; // 3
float f = pi<float>; // 3.14...

variadic模板

C++11允许模板定义处理变化数量的类型参数。为了演示,考虑下面的函数,返回传递给它的任意整数的和。

#include <iostream>
#include <initializer_list>
using namespace std;
int sum(initializer_list<int> numbers)
{
    int total = 0;
    for(auto& i : numbers) { total += i; }
    return total;
}

initializer_list类型表明此函数接受一个花括号封装的列表作为其参数,所以函数必须以如下方式进行调用。

int main()
{
    cout << sum( { 1, 2, 3 } ); // "6"
}

下面的示例将此函数改变为variadic模板函数。这样的函数进行递归遍历而不是迭代遍历,从而一旦第一个参数已被处理,函数会使用其余的参数调用其自身。

variadic模板是使用省略号(…)操作符后跟名字来指定的。这定义了一个所谓的参数包。参数包被绑定到函数中的参数(… rest),然后当函数递归调用其自身时,解包为单独的参数(rest …)。

int sum() { return 0; } // end condition
template<class T0, class ... Ts>
decltype(auto) sum(T0 first, Ts ... rest)
{
    return first + sum(rest ...);
}

此variadic模板函数可以像普通函数一样,使用任意数量的参数进行调用。相对于前面定义的variadic函数,此模板函数可以接受任意类型的参数。

int main()
{
    cout << sum(1, 1.5, true); // "3.5"
}

fold expressions

C++17引入了fold表达式,从而能够在一条语句中将二进制操作符应用于参数包中的所有元素。这可以使得前面的variadic模板函数以更为紧凑的方式进行编写,而无需使用递归。

template<class... T>
decltype(auto) sum(T... args)
{
    // Unpacks to: a1 + (a2 + (a3 + a4))...
    return (args + ...);
}

这里在返回语句中执行了一个一元right fold,由左开始展开参数包,并且在返回结果之前将二元操作符应用于所有参数。通过将省略号放置在参数包的左侧,参数包也可以由右至左展开,如下面使用减法操作符的示例所示。

#include <iostream>
using namespace std;
template<class... T>
decltype(auto) difference(T... args)
{
    // Unpacks to: ...(a1 - a2) - a3
    return (... - args);
}
int main()
{
    cout << difference(5, 2, 1); // "2" (5-2-1)
}

concepts

concept是一个限制模板可以使用哪些模板参数的命名约束集合。它们在C++20中被引入,以允许模板参数在编译时进行类型检测。下面的示例定义了一个名为MyIntegral的concept,要求类型可以转换为完整的整数类型。这里所使用的is_integral_v类模板是标准库的一部分,如果T是一个整型类型,则它被计算为真。

#include <concepts>
#include <type_traits>
// Concept declaration
template <class T>
concept MyIntegral = std::is_integral_v<T>;

此concept可以应用于约束模板参数,例如用于下面的函数模板。用于初始化此函数模板的模板参数必须满足concept的要求,否则将会编辑失败。

template<MyIntegral T>
bool is_positive(T a)
{
    return a > 0;
}
int main()
{
    is_positive(5); // ok, int satisfies MyIntegral
    is_positive("Hi"); // error, string does not satisfy MyIntegral
}

标准库包含了大量预定义concept,当可能时应尽量使用预定义concept而不是用户定义的concept。在此示例中,标准concept std::integral执行与MyIntegral相同的功能,所以前面的函数模板可以重新定义为如下代码。

#include <concepts>
template<std::integral T>
bool is_positive(T a)
{
    return a > 0;
}

有两种表达concept的方法。第一种方法是以条件表达式的形式,这是前面定义的整型concept所用的形式。下面的示例使用整型concept,并且添加了第二个约束以确保类型是带符号的而不是无符号的。注意,此约束利用了使用负数构建无符号类型会返回正值的事实,因为无符号类型不能表达负值。

template <class T>
concept Signed_Integral = std::integral<T> && T{-1} < T{0};

定义concept的第二种方法是使用require子句。此子句定义了要测试的类型的对象,然后是一个或多个约束列表。每个约束由花括号中的表达式后跟期待的返回类型构成。例如,下面的concept声明了类型必须同时实现等于与不等于操作符,而且这些操作的结果必须可以转换为bool。

template<class T>
concept Equal = requires(T a, T b)
{
    { a == b } -> bool;
    { a != b } -> bool;
};
template<Equal T>
bool areEqual(T x, T y)
{
    return x == y;
}
int main()
{
    areEqual(1, 1); // true
}

简记函数模板

在C++20中,通过使用auto占位符类型可以对函数模板进行简记。当auto出现在参数列表中时,函数自动变为函数模板,而auto参数变为其模板参数。将concept应用于这样的函数是通过将concept的名字添加到参数列表中类型的前面来实现的。但要记住,简记函数模板并Visual Studio 2019中并不支持。

#include <concepts>
bool is_positive(std::integral auto a)
{
    return a > 0;
}

int main()
{
    is_positive(2); // calls int version
    is_positive(3L); // calls long version
}

模板lambda

C++14引入了泛型lambda,意味着声明为auto的参数变为模板参数。下面的示例定义了返回向量尺寸的泛型lambda。

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int> v { 1, 2, 3 };
    auto get_size = [](const auto& v) { return size(v); };
    cout << get_size(v); // 3
}

限制此lambda仅作用于向量类型更为可取。C++20中添加了此能力,允许lambda中模板参数的完全使用。

auto get_size = []<typename T>(vector<T> const& v) { return size(v); };