C ++ 之 template 基础

发布于 2023-08-28 21:59:44 字数 18368 浏览 31 评论 0

本文总结了 C++ templates 相关的基础知识,包括如下

  • template definition
  • template argument deduction

分为 function template 和 class template。

Function Template

function template 的定义以 template 关键字开始,后面接着 template 参数列表,后面接着类似常规的函数定义的语法。举个例子说明

template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}

Instantiating a Function Template

一般情况下,当我们调用 function template 时,编译器根据我们提供的参数来自动推导 template 参数列表,例如

cout << compare(1, 0) << endl;    // T is int

编译器自动推导 template 参数列表为(T, int),当 template 推导出来之后,会自动地去实例化一份函数代码。举上面的 int 的例子,它会自动地实例化下面的代码

int compare(const int &v1, const int &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}

当 compare 调用其他推导出来的类型时,也会自动地实例化对应的函数版本,例如

vector<int> vec1{1, 2, 3};
vector<int> vec2{1, 2, 3};
cout << compare(vec1, vec2) << endl; // T is vector<int>

int compare(const vector<int> &v1, const vector<int> &v2)
{
//....
}

Template Type Parameters

在 function template 中,可以使用 template type parameters 来作为函数参数类型,返回值类型以及函数内部定义类型,例如

template <typename T> T foo(T* p)
{
T tmp = *p;
// ...
return tmp;
}

在较老的 C++标准中,还没有 typename 关键字,之前是用 class 关键字来当 typename 用的。不过在支持 typename 关键字的版本中,还是推荐使用 typename。

Nontype Template Parameters

在 function template,我们也可以用 Nontype Template Parameters,表示我们对某个 type parameters 使用固定类型的参数。

在函数实例化时,nontype template parameters 应该使用常量表达式作为参数,从而让编译器在编译期间推导出它的值。

举个例子,我们想要比较字符串常量,这些字符串常量以 const char 开头。因为我们不能拷贝数组,所以,我们的函数参数定义为数组的引用,同时,需要能处理各种不同长度的类型,因此,定义两个 nontype template parameters,第一个代表第一个数组的长度,第二个代表第二个数组的长度。

template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}

当我们调用 compare("hi", "mom") 时,会实例化如下代码

int compare(const char (&p1)[3], const char (&p2)[4]);

inline and constexpr Function Templates

inline 和 constexpr 关键字放在 template argument 之后,函数返回值之前,如下

// ok
template <typename T> inline T in(const T &, const T &)

//error
inline template <typename T> T min(const T &, const T &)

Writing Type-Independent Code

从 compare 可以看出,有两个比较重要的原则可以帮助我们写出更通用的 function template

  • 函数参数的类型为 reference to const
  • 比较只用了 <

使用了 reference to const,我们可以保证函数可以用于不能拷贝的类型;只使用 < ,使得我们的函数只要求类型实现了 < 运算符。

为了更进一步地提高通用性,我们可以用 less 函数来进行比较,使得我们的函数对指针也适用

template <typename T> int compare(const T &v1, const T &v2)
{
if (less<T>()(v1, v2)) return -1;
if (less<T>()(v2, v1)) return 1;
return 0;
}

Template Compilation

模板实例化只在编译器看到了我们使用模板的时候才做,并且实例化的时候,编译器还需要看到模板的代码,因此,一般模板源码放到头文件中。

Compilation Errors Are Mostly Reported during Instantiation

编译器编译模板代码的三个步骤

  1. 编译模板本身,这时候编译器一般可以检查一些语法错误
  2. 当编译器看到使用模板时,这个时候会检查一些函数参数个数是否匹配,类型是否一致等信息
  3. 当编译器真正实例化时,剩下的编译错误才会被报出来

举个例子

Sales_data data1, data2;
cout << compare(data1, data2) << endl;

这个调用用 Sales_data 来替换 T,这里面需要使用 < ,但是 Sales_data 并不支持,因此会报错,但这个错误只有到编译器实例化模板的时候才会报出来。

Class Template

class template 和 function template 不同的是,class template 必须显式地提供模板参数类型。

Defining a Class Template

先是模板参数列表,然后是 class 本身,例如

template <typename T> class Blob {
public:
typedef T value_type
typedef typename std::vector<T>::size_type size_type;
Blob();
Blob(std::initializer_list<T> i1);
void push_back(const T &t) {data->push_back(t);}
}

Instantiating a Class Template

为了实例化一个 class template,我们需要显式地提供类型信息,例如

Blob<int> ia; // Blob<int>
Blob<int> ia2 = {0, 1, 2, 3, 4}

编译器会实例化以下代码

template <> class Blob<int> {
typedef typename std::vector<int>::size_type size_type;
Blob();
Blob(std::initializer_list<int> i1);
int& operator[](size_type i);

private:
std::shared_ptr<std::vector<int>> data;
void check(size_type i, const std::string &msg) const;
}

每个实例化会产生一个独立的 class,例如 Blob<string> 跟其他的 Blob 类型没有任何的关系。

Member Functions of Class Templates

定义的语法为

template <typename T>
ret-type Blob<T>::member-name(param-list)

一个具体的例子

template <typename T>
void Blob<T>::check(size_type i, const std::string &msg)
{
if (i >= data->size()) {
throw std::out_of_range(msg);
}
}

Instantition of Class-Template Member Functions

一般地,只有程序使用了 Class Template 的成员函数,该成员函数才会被实例化。

Simplifying Use of a Template Class Name inside Class Code

在一个 class template 内部,我们可以省略掉模板参数,例如

template <typename T> class BlobPtr
public:
BlobPtr(): curr(0) {}
BlobPtr& operator++()
BlobPtr& operator--()

Using a Class Template outside the Class Template Body

在 class template 外部使用时,必须要带上模板参数,例如

template<typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
BlobPtr ret = *this
++*this
return ret;
}

BlobPtr ret 就相当于 BlobPtr<T> ret

Class Templates and Friends

  • 一个 class template 如果有一个非 template 类型的友元,那么该友元对于 class template 的所有实例都生效
  • 如果一个 class template 有 template 类型的友元,则可以通过控制来决定友元的作用范围

One-to-One FriendShip

最常见的是友元关系是一个 class template 和另一个 class template 以同样模板参数实例化的类互为友元类,例如

template <typename T> class BlobPtr;
template <typename T> class Blob;
template <typename T>
bool operator==(const Blob<T>&, const Blob<T> &);
template <typename T> class Blob {
friend class BlobPtr<T>
friend bool operator==<T>
(const Blob<T> &, const Blob<T> &)
}

以相同模板类型初始化的 Blob 和 BlobPtr 互为友元类,例如

Blob<int> ca; // BlobPtr<char> and operator==<char> are friends
BlobPtr<int> ia; // BlobPtr<int> and operator==<int> are friends

General and Specific Template FriendShip

通过控制,还能配置更一般地友元关系,如下

template <typename T> class Pal;
class C {
friend class Pal<C>; // Pal<C> is a friend to C
template <typename T> friend class Pal2; // all instance of Pal2 are friend to C
}

template <tyname T> class C2 {
friend class Pal<T>;
template <typename X> friend class Pal2; //all instances of Pal2 are friends of each instance of C2
friend class Pal3; // Pal3 is friend of every instance of C2
}

为了使得所有的实例都是友元,友元的声明必须以不同的模板参数声明。

Befriending the Template’s Own Type Parameter

在 C++11 标准下,支持以下语法

template <typename Type> class Bar {
friend Type;
}

其中 Type 可以是内置的类型。

Template Type Aliases

我们可以使用 using 语法来创建 template 别名。

template <typename T> using twin = pair<T, T>
twin<string> authors;

template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books;
partNo<string> cars;
partNo<Student> kids;

static Members of Class Templates

class template 可以定义静态类型,每个实例化的类拥有自身的 static 成员函数,例如

template <typename T> class Foo {
public:
static std::size_t count() { return ctr; }
private:
static std::size_t ctr;
}

Template Parameters

Template Parameters and Scope

模板参数使用的名称,在 template 内部不能再使用。

typedef double A;
template <typename A, typename B> void f(A a, B b)
{
A tmp = a; // has the same type with template arugment A
double = B; // error
}

Template Declarations

template declaration 一定要包含 template parameters,例如

template <typename T> int compare(const T&, const T &)
template <typename T> class Blob;

Using Class Members That are Types

假如 T 是模板参数,那么当编译器看到以下语句时

T::size_type *p;

它需要知道这是定义一个新的指针,还是把 size_type 和 p 相乘。默认地,编译器会认为这个不是类型定义,因此,如果是类型定义的话,必须要显式地指明。

typename T::size_type *p;

Default Template Arguments

可以为模板参数指定默认值,例如

template<typename T, typename F = 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;
}

Template Default Arguments and Class Templates

当一个 class template 的所有模板参数都带默认值时,我们定义类时,需要带一个 <> ,例如

template <class T = int> class Numbers {
public:
Numbers(T v = 0) : val(v) {}
private:
T val;
}
Numbers<long double> lots_precision;
Numbers<> average_precision; // empty, T = int

Member Templates

成员函数本身也可能是模板,分为 class template 和 non class template 两种情况讨论。

Member Templates of Ordinary(Nontemplate) Classes

举个例子,和 unique_ptr 的默认删除器的实现有关,如下

class DebugDelete {
public:
DebugDelete(std::ostream &s = std::cerr) : os(s) {}
template <typename T> void operator()(T *p) const
{
os << "deleting unique_str" << std::endl;
delete p;
}
private:
std::ostream &os;
}

使用方法如下:

double *p = new double;
DebugDelete d;
d(p); // calls DebugDelete::operator()(double *)
int *ip = new int;
DebugDelete()(ip); //operator()(int *)

Member Templates of Class Templates

和 class template 的普通函数不同,member template 是 function template,在定义时,还得带上函数本身的模板参数,如下

template <typename T> class Blob {
template <typename It> Blob(It b, It e);
}

template <typename T>
template <typename It>
Blob<T>::Blob(It b, It e):
data(std::make_shared<std::vector<T>(b, e)>) {

}

Instantiation and Member Templates

对于 member template,对于类的模板参数是要指定的,而其本身的模板参数一般是通过函数参数推断出来的。

Controlling Instantiations

当两个或多个独立的源代码文件使用了相同参数的模板,每个源代码文件中都会有该模板的一份实例化的代码。

在大型项目中,这会造成代码体积变大,编译变慢。在 C++11 中,可以通过如下方法来避免该问题

extern template declaration;

template declaration;

一般地,可以把模板实例化的代码放到一个单独的文件中,如下

template int compare(const int&, const int&);
template class Blob<string>;

需要注意的是,这种声明会把整个类中的所有函数都实例化。

Template Argument Deduction

主要描述 function template 的参数推导规则。

Conversions and Template Type Parameters

编译器实例化模板时,会考虑以下转换规则:

  • const 转换:当函数参数是 const 引用时,一个非 const 对象是可以作为参数传入的
  • 数组或函数到指针的转换:一个数组会被转换成一个元素的指针,函数则会转成函数指针

例如

template <typename T> T fobj(T, T);
template <typename T> T fref(const T&, const T&);
string s1("a value")
const string s2("another value")

fobj(s1, s2); // fobj(string, string),const 被忽略
fref(s1, s2); // fref(const string &, const string &)

int a[10], b[42];
fobj(a, b); // f(int *, int *)
fref(a, b); // error:数组无法转换成引用

第一个 fobj(s1,s2),const 能忽略的原因是,因为 s1 和 s2 都会拷贝到函数参数中,所以,原来是否是 const 不影响。

Function Parameters That Use the Same Template Parameter Type

以前面的 compare 函数为例

long lng;
compare(lng, 1024); //错误:无法实例化 compare(long, int)

Normal Conversions Apply for Ordinary Arguments

普通的参数没有特殊的转换,跟之前的函数转换规则一样。

Function Template Explicit Arguments

function template 也可以显式地指定模板参数。

Specifying an Explicit Template Argument

例如,一个加法运算,用户可以指定返回值类型,来控制运算的精度。

template <typename T1, typename T2, typename T3> T1 sum(T2, T3);

在这种情况下,T1 是无法被推导出来的,只能是调用者显式地指定。

Normal Conversions Apply for Explicitly Sepecified Arguments

对于指定了类型的模板函数,是可以采用通用的类型转换规则的

long lng;
compare(lng, 1024); // error
compare<long>(lng, 1024); // ok
compare<int>(lng, 1024); //ok

Trailing Return Type and Type Transformation

可以使用尾置返回类型,从函数参数中推导出返回类型,从而避免用户需要显式地指定返回类型,例如

template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
//process
return *beg;
}

上述函数中,返回类型为 beg 指向的类型的引用。

The Type Transformation Library Template Classes

可以通过 remove_reference 来实现函数返回一个值,而非引用,如下

template <typename It>
auto fcn2(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type
{
//process the range
return *beg;
}

除了 remove_reference ,我们还可以有 remove_pointer 等实现各种功能的 type transformation 函数。

Function pointers and Argument Deduction

通过函数指针赋值,可以直接实例化一个模板函数,如下

template <typename T> int compare(const T&, const T&);
int (*pf1)(const int&, const int&) = compare

上面会直接实例化参数 T 为 int 的 compare 函数

Template Argument Deduction and References

分函数参数为左值和右值两种情况讨论。

Type Deduction from Lvalue Reference Function Parameters

当函数参数为左值引用时,可以传入的参数为左值,可以为 const 类型,如下

template <typename T> void f1(T &);
f1(i); // T is int
f1(ci); // T is const int
f1(5); // error must be lvalue

Type Deduction from Rvalue Reference Function Parameters

当函数参数为右值引用时,如下

template <typename T> void f3(T&&);
f3(42); // T is int

Reference Collapsing and Rvalue Reference Parameters

当传入一个 int 左值到 f3 函数时,正常情况下来看,由于 i 是左值,应该不能绑定到右值参数上。但是,有两个特殊的绑定规则,可以支持传入左值:

  • 当我们传入左值到右值引用时,模板参数类型会被推导成左值引用
  • 由于传入的参数被推导成引用,而函数参数也是引用,会产生引用堆叠,这个有特殊的规则

引用堆叠的规则

  • X& &,X& &&和 X&& &会被看成 X&
  • X&& &&会被看成 X&&

Writing Template Functions with Rvalue Reference Parameters

上述规则会造成以下代码产生奇怪的现象

template <typename T> void f3(T&& val)
{
T t = val;
t = fcnt(t);
if (val == t) {}
}
  • 当传入的参数是右值时,T 是 int
  • 当传入的参数是左值时,T 是 int&

一般右值引用参数用于两种场景,参数转发和模板重载。

Understanding std::move

std::move 的定义如下

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_feference<T>::type&&>(t);
}

std::move 的工作原理:

当传入右值时,例如 string(“test”),其工作流程如下

  • 推导出来的类型 T 为 string
  • 因此,remove_reference 实例化为 string
  • remove_reference 为 string
  • move 的返回类型为 string &&
  • 而 move 的函数参数 t,为 string &&

这时候实例化的 move 函数为

string && move(string &&t);

函数返回 return static_cast<string &&>(t) ,由于 t 本身就是 string && ,因此,函数会返回传入的右值引用。

当传入左值时,其工作流程如下

  • 推导出来的类型 T 为 string &
  • remove_reference 实例化为 string &
  • remove_reference<string &>为 string
  • move 的返回类型还是 string &&
  • move 的函数参数,实例化为 string & &&,堆叠成 string &

这次调用会实例化 move 函数为

string&& move(string &t);

函数返回值为 static_cast<string &&>(t) ,因为 t 的类型为 string & ,因此,会转换成 string &&

Forwarding

有些函数需要转发一些参数,且要保留它们的类型信息,包括左值右值,const 类型信息等。

以一个例子来说明,如下

template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}

当 f 中有引用类型时,会有奇怪的现象

void f(int v1, int &v2)
{
cout << v1 << " " << ++v2 << endl;
}

f(42, i); //f 改变参数 i
flip1(f, j, 42); //flip1 并没有改变 j

flip1 推导出 T1 是 int,然后,传递给 f 的是函数左值参数,因此,并不会改变外面传进入的 j。

即,如下

void flip1(void (*fcn)(int, int &), int t1, int t2);

Defining Function Parameters That Retain Type Information

可以使用右值引用来解决上述问题,如下

template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}

当再次考虑调用如下是

flip1(f, j, 42);

其中 T1 会被推导成 int & ,然后根据堆叠规则,t1 的类型为 int & ,因此,在 f 中是能增加 j 的值的。

上述函数在左值时能正常工作,但是,在右值引用时,无法正常工作,如下

void g(int &&i, int &j)
{
cout << i << " " << j << endl;
}

flip2(g, i, 42); //error

其中,42 推导出 T2 为 int,然后 t2 是 int && ,注意,只是类型是 int && ,但它本身是一个 lvalue,所以,不能绑定到 g 的第一个参数。

Using std::forward to Preserve Type Information in a Call

可以使用 std::forward 解决上述问题, std::forward<T> 的返回值为 T && ,如下

template <typename Type> intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
}
  • 当传入的参数是是 rvalue,Type 的类型是 rvalue 的类型,那么, forward<Type> 将返回 Type &&
  • 当出软的参数是 lvalue 时,那么 Type 是 lvalue reference,即 Type &,则 forward<Type> 则是 &&& 堆叠,最后,返回的还是 lvalue reference

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

别挽留

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

qq_E2Iff7

文章 0 评论 0

Archangel

文章 0 评论 0

freedog

文章 0 评论 0

Hunk

文章 0 评论 0

18819270189

文章 0 评论 0

wenkai

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文