Chapter 6 Templates – A Tour of C++

6.2 Parameterized Types

现在我们可以将double类型的vector转换为任何类型的vector,只需要使用template替代之前几个章节中double类型。

template<typename T>
class Vector
{
private:
    T* elem;
    int sz;
public:
    explicit Vector(int s);
    ~Vector() { delete[] elem; }

    T& operator[](int i);
    const T& operator[](int i) const;
    int size() const { return sz; }
};

此外,我们还能使用class代替typename,其含义与作用和typename相同。当我们使用template之后,其相应的成员函数也需要做出一定的修改。

template<typename T>
Vector<T>::Vector(int s)
{
    if(s < 0)
        throw Negative_size{};
    
    elem = new T[s];
    sz = s;
}

template<typename T>;
const T& Vector<T>::operator[](int i) const
{
    if(i < 0 || size() <= i)
        throw out_of_range{"Vector::operator[]"};

    return elem[i];
}

template<typename T>
T* begin(Vector<T>& x)
{
    return x.size() ? &x[0] : nullptr;
}

template<typename T>
T* end(Vector<T>& x)
{
    return x.size() ? &x[0] + x.size() : nullptr;
}

需要注意的是,template属于编译时刻的机制,所以在运行时其并不会造成额外的开销。事实上,Vector生成的代码与之前的Vector所生成的代码是相同的。

6.2.1 Constrained Template Arguments(C++20)

一般来说,只有当template的参数满足特点的标准,template才能生效。例如,一个Vector会进行拷贝运算,那么其潜在的标准就是其中的每一个元素都能够被拷贝。因此,我们要求Vector的template参数应该是一个Element(c++关键词),Element表示该类型的实例化对象能够成为一个元素。

template<Element T>
class Vector
{
private:
    T* elem;
    int sz;
};

Vector<int> v1;    // 运行ok,因为int能够被拷贝
Vector<thread> v2; // error!!! 标准的线程不能被拷贝

在上述代码中,我们试着去实例化一个错误的template,那么就会收到编译时刻的报错。但是c++20之前的版本并不支持该特性。

6.2.2 Value Template Arguments

template除了拥有类型参数还能拥有值参数。

template<typename T, int N>
struct Buffer
{
    using value_type = T;
    constexpr int size() { return N; }
    T[N];
};

Buffer<char, 1024> glob; // 全局变量,静态分配

void fct()
{
    Buffer<int, 10> buf; // 局部变量,存储在stack
}

上述代码中的value_type与constexpr能够让用户读取到template的参数(只读)。此外,template的值类型参数在很多情况下非常有用,这样我们就能创建随机长度的Buffer,但是并不需要使用动态内存。不过需要注意的是,template的值参数必须是const expression(常量表达式)。

6.3.1 Function Templates

我们可以创建一个函数计算任何序列中的元素的总和。

template<typename Sequence, typename Value>
Value sum(const Sequence& s, Value v)
{
    for(auto x : s)
        v += x;

    return v;
}

void user(Vector<int>& vi, list<double>& ld, vector<complex<double>>& vc)
{
    int x = sum(vi, 0);
    double d = sum(vi, 0.0);
    double dd = sum(ld, 0.0);
    auto z = sum(vc, complex{0.0, 0.0});
}

template函数可以是成员函数,但是其不能是一个虚成员函数。因为编译器无法识别程序中template的所有实例化,这样也意味着编译器无法生成vtbl。

6.3.2 Function Objects

template的另一种使用方法是function object(函数对象,或者说functor)。

template<typename T>
class Less_than
{
    const T val; // 比较对象
public:
    Less_than(const T v) : val{v} {}
    bool operator()(const T& x) const { return x < val; }
};

Less_than lti {42}; 
Less_than lts {"Backus"s};
Less_than<string> lts2 {"Naur"}; // 因为"Naur"是C类型的string,所以我们需要<string>将其进行转换

void fct(int n, const string& s)
{
    bool b1 = lti(n); // true,如果n<42
    bool b2 = lts(s); // true,如果s<"Backus"
}

上述代码中名为operator()的函数通过运算符()来进行函数调用。类型为Less_than的三个变量就是function object,其能广泛地用于函数的参数。

template<typename C, typename P>
int count(const C& c, P pred)
{
    int cnt = 0;
    for(const auto& x : c)
        if(pred(x))
            ++cnt;

    return cnt;
}

void f(const Vector<int>& vec, const list<string>& lst, int x, const string& s)
{
    cout << "number of values less than " << x << ": " << count(vec, Less_than{x}) << '\n';
    cout << "number of values less than " << s << ": " << count(lst, Less_than{s}) << '\n';
}

上述代码中Less_than{x}构造了类型为Less_than的对象,其能够比较两个int的大小。而Less_than{s}也有着相同的意义。而function object的优势在于,其能够携带比较的对象。我们不需要为每一个值去创建独立的函数,也不需要引入全局变量来保存某一个值。此外,对于Less_than这种简单的function object,其默认是inline的,因此调用Less_than的效率远远高于一次间接的函数调用。

6.3.3 Lambda Expressions

上文中我们使用了Less_than来定义function object,不过还有另一种方法能够隐示地生成function object

void f(const Vector<int>& vec, const list<string>& lst, int x, const string& s)
{
    cout << "number of values less than " << x << ": " 
        << count(vec, [&](int a){ return a < x; }) 
        << '\n';
    cout << "number of values less than " << s << ": " 
        << count(lst, [&](const string& a){ return a < s; }) 
        << '\n';
}

上述函数中的[](int a){ return a < x; }被称为lambda表达式。其生成的function object类似于之前的Less_than{x}。[&]是抓取列表,其表示lambda体中所使用的局部变量都会通过引用来读取。如果我们只想抓取变量x,那么我们可以使用[&x]。如果我们想要生成的对象是x的拷贝,那么我们可以使用[=x]。此外,[]表示不抓取任何变量。

使用lambda表达式能够使我们的代码更为精简,但是也会使代码更为难懂。因此建议大家为lambda表达式命名,这样能够准确表达它的用处。

template<typename C, typename Oper>
void for_all(C& c, Oper op) // 假设C是一组指针的容器
{
    for(auto& x : c)
        op(x);
}

void user2()
{
    vector<unique_ptr<Shape>> v;
    while(cin)
        v.push_back(read_shape(cin));

    for_all(v, [](unique_ptr<Shape>& ps){ ps->draw(); });
    for_all(v, [](unique_ptr<Shape>& ps){ ps->rotate(45); });
}

上述代码中,我们定义了for_all函数,其用于处理所有容器的遍历操作。由于使用了unique_ptr&作为参数传入lambda,所以for_all并不需要考虑参数是如何存储的。此外,for_all并不会影响Shape对象的生命周期,lambda的函数体使用指针的方式类似于“裸体”指针。

template<class S>
void rotate_and_draw(vector<S>& v, int r)
{
    for_all(v, [](auto& s){ s->rotate(r); s->draw(); });
}

上述代码使得lambda表达式的应用更为范例化。auto意味着任何类型都能作为参数传入,从某种程度上来说,这使得lambda表达式变为了一种template函数,我们称其为generic lambda。不过在普通的函数定义中,我们还不能将auto作为参数类型。

现在任何类型的容器都能作为rotate_and_draw的参数。

void user4()
{
    vector<unique_ptr<Shape>> v1;
    vector<Shape*> v2;

    rotate_and_draw(v1, 45);
    rotate_and_draw(v2, 90);
}

通过lambda表达式,我们可以将“状态”转换为表达式。以下代码是典型的初始化的过程,但显然其并不标准。对于数据结构v我们需要进行不同的操作,同时代码显得非常混乱且容易造成bug。

enum class Init_mode { zero, seq, cpy, patrn };

vector<int> v;

switch(m)
{
    case zero:
        v = vector<int>(n);
        break;
    case cpy:
        v = arg;
        break;
};

if(m == seq)
    v.assign(p, q);

但是,我们可以将lambda作为一个初始化过程。

vector<int> v = [&] {
    switch (m)
    {
        case zero:
            return vector<int>(n);
        case seq:
            return vector<int>{p,q};
        case cpy:
            return arg;
    }
};

6.4 Template Mechanisms

为了定义优秀的template,我们还需要其他的一些工具:

  • 基于类型的值:variable template
  • 类型与模板(template)的别名:alias template
  • 编译器选择机制:if constexpr
  • 编译器“询问”类型与表达式:requires-表达式

6.4.1 Variable Templates

当我们在class template中定义一个const,我们可能希望const的值随着T的变化而变化。

template<class T>
    constexpr T viscosity = 0.4;

template<class T>
    constexpr space_vector<T> external_acceleration = { T{}, T{-9.8}, T{} };

auto vis2 = 2 * viscosity<double>;
auto acc = external_acceleration<float>;

6.4.2 Aliase

在标准库的头文件cstddef中包含了对于size_t的定义,例如using size_t = unsigned int。也就是说,size_t的实际类型可能会随着代码的实现而改变,在其他库中size_t的类型可能是unsigned long。因此,size_t使程序员能够更为灵活地掌控代码。

template<typename T>
class Vector
{
public:
    using value_type = T;
};

6.4.3 Complie-Time if

假设我们需要实现某种运算,其包含两种处理,分别为slow_and_safe(T)和simple_and_fast(T)。其通常会基于性能进行选择。传统的方式可能就是创建一个重载函数,基于标准库中的is_pod进行判断,如果是一个基类class,那么我们就进行slow_and_safe。如果是子类class,那么我们就使用重载的simple_and_fase。但是,在c++17中,我们可以使用编译器的if判断。

template<typename T>
void update(T& target)
{
    if constexpr(is_pod<T>::value)
        simple_and_fast(target);
    else
        slow_and_safe(target);
}

6.5 Advice

[01] 对于算法表达式,使用template使其适用于大多数参数类型。

[06] 使用function object作为算法的参数。

[07] 如果你只需要在代码中的一处使用function object,那么请使用lambda。

[08] 虚函数不能为template函数。

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据