C++之旅(学习笔记)第7章 模板

C++之旅(学习笔记)第7章 模板

模板是一个类或者一个函数,我们用一组类型或值对其进行参数化。

7.1 参数化类型

template 
class Vector {
private:
    T* elem;
    int sz;
public:
    explicit Vector(int s);
    ~Vector(){ delete[] elem;}
    T& operator[](int i);				//对于非常量的Vector
    const T& operator[](int i) const; 	//对于常量Vector
    int size() const {return sz;}
}

前缀template 指明T是该声明的形参。在引出类型参数时,使用class和使用typename是等价的,在旧式代码中经常出现将template 作为前缀。

成员函数的定义方式与之类似:

template
Vector::Vector(int s)
{
    if(s < 0)
        throw length_error{"Vector constructor: negative size"};
    elem = new T[s];
    sz = s;
}
template
const T& Vector::operator[](int i) const
{
    if(i < 0 || size() <= i)
        throw out_of_range{"Vector::operator[]"};
    return elem[i];
}

可以用如下方式声明动态数组Vector:

Vector vc(200);		// 200个字符组成的动态数组
Vector vs(17);		// 17个字符串组成的动态数组
Vector> vli(45);	// 45个整数链表组成的动态数组

为了让Vector支持范围for循环,需要为之定义适当的begin()和end()函数:

template
T* begin(Vector& x)
{
    return &x[0];			// 指向第一个元素,或者指向末尾元素后面的一个位置
}
template
T* end(Vector& x)
{
    return &x[0]+x.size();	// 指向末尾元素后面的一个位置
}
  • 模板是一种编译时的机制,因此与“手工编码”相比,它并不会产生任何额外的运行时开销。
  • 模板加上一系列模板参数被统称为实例化或者特例化。在编译过程中进行实例化时,每个实例都会生成一份代码。

7.2 受限模板参数

Vector的模板参数仅仅是一个typename还不够,还需要指定Element满足特定需求才能成为其元素:

template
class Vector {
private:
    T* elem;
    int sz;
    // ...
};

这里,templateElement是一个谓词,用于检查T是否满足Vector需要的特性。

这种谓词叫做概念。在模板参数中指定一个概念,这叫作受限模板参数,拥有这种参数的模板叫作受限模板。

7.2.1 模板参数值

除了类型参数以外,模板还支持值作为参数。

template
struct Buffer{
    constexpr int size() {return N;}
    T elem[N];
    // ...
};

值参数在很多环境中都有用。例如:Buffer允许我们创建任意尺寸的缓冲区而不需要使用动态内存分配:

Buffer glob;		// 静态分配的全局char缓冲区
void fct()
{
    Buffer buf;		// 栈上分配的本地int缓冲区
    // ...
}

但是,字符串字面量不可以作为模板值参数。但我们可以使用存放字符的数组来表示字符串:

template
void outs()
{ 
    cout << s; 
}

char arr[] = "Werid workaround";
void use()
{
    outs <"straightforward use">();		// 到目前为止,这样不可行
    outs();						// 诡异的间接解决方案
}

7.2.2 模板参数推导

指定模板参数类型显得有些冗长,考虑使用标准库模板pair:

pair p = {1, 5.2};
// 等价于
pair p = {1, 5.2};

可以使用s后缀把字符串变成一个正确的string

template
class Vector {
public:
    Vector(int);
    Vector(initializer_list);		// 初始化列表构造函数
    // ...
};

Vector vs {"Hello", "World"};	// 可行:Vector
Vector vs1 {"Hello"s, "World"s};		// 可行:推导为Vector
Vector vs2 {"Hello"s, "World"};			// 错误:初始化列表中的数据类型不一致

7.3 参数化操作

模板还被广泛用于参数化标准库中的类型与算法。

要想表达将操作用类型或者值来参数化,有三种方法:

  • 模板函数。
  • 函数对象:对象可以携带数据,并且以函数的形式调用。
  • 匿名函数表达式:函数对象的简略记法。

模板函数可以是成员函数,但不能是virtual函数。编译器不可能知道模板的所有实例,所以不可能像函数一样被调用。

7.4 函数对象

一种特别有用的模板叫作函数对象(有时也被称为仿函数),它可以用来定义对象,该对象可以像函数一样被调用。

template
class Less_than{
    const T val;
public:
    Less_than(const T& v):val(v){}
    bool operator()(const T& x) const {return x < val;}	//函数调用操作符
};

名叫operator()的函数实现了应用操作符(),这个操作符又可被称作“函数调用操作符”“调用操作符”。

7.5 匿名函数表达式(lambda)

匿名函数表达式(lambda)

[&](int a){ return a < x; }

[&]是匿名函数的捕获列表,它指定了函数体内所有局部变量可以以引用的形式被访问。

  • 只捕获x这一个变量,使用[&x];
  • 生成x的一份拷贝,使用[x];
  • 什么都不捕获,使用[];
  • 以引用的方式捕获所有局部变量,使用[&];
  • 以值的方式捕获所有局部变量,使用[=];
  • 如果成员函数中定义匿名函数,使用[this]来捕获当前对象;
  • 如果成员函数中定义匿名函数,可以使用[*this]捕获当前对象的拷贝。

7.6 作用域终结函数

析构函数提供了一种通用的方案,用于在作用域结束时隐式清除所有使用过的对象RAII,但如果需要进行的清理涉及多个对象,或者涉及不含析构函数的对象,可以定义一个finally()函数,它在作用域结束时执行:

void old_style(int n)
{
    void *p = malloc(n* sizeof(int));
    auto act = finally([&]{free(p);});	// 作用域结束时调用该匿名函数
}

实现finally()函数的方法:

template
[[nodiscard]] auto finally(F f)
{
    return Final_action{f};
}

这里使用了[[nodiscard]]属性修饰,确保用户不会忘记保存所生成的返回值Final_action,因为正常完成功能必须保存它。

用来提供析构函数的类Final_action可以写成下面这样:

template
struct Final_action{
    explicit Final_action(F f):act(f){}
    ~Final_action(){act();}
    F act;
};

7.7 模板机制

  • 依赖类型的值:参数模板
  • 类型与模板的别名:别名模板
  • 编译时选择机制:if constexpr
  • 编译时查询值与表达式属性的机制:requires表达式

7.8 建议

  1. 用模板来表达那些可以用于多种参数类型的算法;
  2. 用模板实现容器;
  3. 用模板提升代码的抽象层次;
  4. 让构造函数或函数模板推断出类模板实参类型;
  5. 把函数对象作为算法的参数;
  6. 如果简单的函数对象只在某处使用一次,不妨使用匿名函数;
  7. 不能把虚函数成员定义成模板成员函数;
  8. 使用finally()为不带析构函数且需要“清理操作”的类型提供RAII;
  9. 利用模板别名来简化符号并隐藏实现细节;
  10. 使用if constexpr条件编译提供替代实现,不会存在运行时开销;

你可能感兴趣的:(C++,c++,学习,笔记)