欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

模板基础知识1——《C++程序设计语言(第四版)》第23章 模板 笔记

程序员文章站 2022-07-12 17:49:18
...

基础知识

  1. 模板实例化是从一个模板和一组模板实参来生成代码,模板实例化生成的代码与程序员手工扩展模板得到的代码完全一样。
  2. 模板机制最大的弱点是无法直接表达对模板实参的要求。
  3. 对于一个模板使用不同模板实参生成的类型是不同类型,例如,假定Circle是一种Shape:
vector<Shape> vs{vector<Circle>{}}; //错误
vector<Shape*> vs{vector<Circle*>{}}; //错## 标题 ##误

类模板成员

模板类可以有几种不同类型的成员

数据成员

非static数据成员可以在定义时初始化,也可在构造函数中初始化

template<typename T>
struct X {
    int m1 = 7;
    T m2;
    X(const T &x):m2{x}{}
};
X<int> xi{9};
X<string> xs{"123"}

非static数据成员可以是const的,但却不能是constexpr的。

成员函数

非static成员函数的定义可以在类模板内部,也可在外部,如:

template<typename T>
struct X{
    void mf1(){/*...*/} //类内定义
    void mf2();
};

template<typename T>
void X<T>::mf2(){/*...*/} //类外定义

类模板成员函数可以是virtual的,也可以不是。但是,一个虚成员函数名不能再用作一个成员函数模板名。

成员类型别名

模板参数名T只能被模板自身访问,如果其他代码想使用元素类型,可提供一个别名,如:

template<typename T>
class Vector {
public:
    using value_type = T;
    using iterator = Vector_iter<T>; Vector_iter是在别处定义的
}

static成员

一个类外定义的static数据或函数成员在整个程序中只能有唯一一个定义。如:

template<typename T>
struct X {
    static constexpr Point p {100, 250}; //Point必须是一个字面值常量类型
    static const int m1 = 7;
    static int m2 = 8;  //错误:不是const
    static int m3;
    static void f1(){/*...*/}
    static void f2();
};

template<typename T> int X<T>::m1 = 88; //错误:有两个初始化器
template<typename T> int X<T>::m3 = 99;
template<typename T> void X<T>::f2(){/*...*/} 

一个static成员只有真被使用时才需要定义,如:

template<typename T>
struct X {
    static int a;
    static int b;
};

int *p = &X<int>::a;

此时编译器会报告X::a“未定义”,而对X::b就不会。

成员类型

template<typename T>
struct X {
    enum E1{a, b};
    enum E2; //错误:基础类型未知
    enum class E3;
    enum E4 :char;

    struct C1 {/*...*/};
    struct C2;
};

template<typename T>
enum class X<T>::E3 {a, b}; //必须的

template<typename T>
enum class X<T>::E4:char {x, y}; //必须的

template<typename T>
struct X<T>::C2 {/*...*/}; //必须的

成员模板

template<typename Scalar>
class complex {
    Scalar re, im;
public:
    complex():re{}, im{}{}      //默认构造函数
    template<typename T>
    complex(T rr, T ii = 0) : re{rr}, im{ii}{}

    complex(const complex&) = default;
    template<typename T>
        complex(const complex<T>& c) : re{c.real()}, im{c.imag()}{}
    //...
};

这种定义允许数学上有意义的复数类型转换,同时禁止不合需要的窄化转换

complex<float> cf; //默认值
complex<double> cd{cf}; //正确,float向double转换
complex<float> cf2{cd}; //错误,double向float转换

complex<float> cf2{2.0, 3.0}; //错误,窄化转换禁止
complex<double> cd2{2.0F, 3.0F}; //正确
  1. 成员模板不能是virtual的。如果C++允许这样的代码,用于实现虚函数机制的传统虚函数表技术就无法使用了。
  2. 在模板中尽量避免嵌入类型,除非他们真正依赖于所有模板参数,否则会引起代码膨胀

友元

Matrix和Vector相乘的例子

template<typename T> class Matrix;

template<typename T>
class Vector {
    T v[4];
public:
    friend Vector operator*<>(const Matrix<T>&, const Vector&);
    //...
};

template<typename T>
class Matrix{
    Vector<T> v[4];
public:
    friend Vector operator*<>(const Matrix<T>&, const Vector&);
    //...
};

友元函数后面的<>是必须的,它清楚地指出友元函数是一个模板函数。若没有<>,友元函数将被假定是非模板函数。

template<typename T>
Vector<T> operator*(const Matrix<T> &m, const Vector<T> &v)
{
    Vector<T> r;
    return r;
}

函数模板

函数模板的声明和定义可在不同位置

函数模板实参

如果不能从函数实参推断出一个模板实参,就必须显示指定它,如:

template<typename T>
T* create();  //创建一个T,返回指向它的指针

void f()
{
    int *p = create<int>(); //函数模板,实参为int
    int *q = create();  //错误:无法推断模板实参
}

函数模板实参推断

template<typename T>
class Xref {
public:
    Xref(int i, T *p)  //保存一个指针:Xref是所有者
        :index{i}, elem{p}, owner{true}
    {}

    Xref(int i, T& r)  //保存一个指向r的指针,所有者是其他人
        :index{i}, elem{&r}, owner{false}
    {}

    Xref(int i, T &&r) //将r移入Xref,Xref变为所有者
        :index{i}, elem{new T{move(r)}}, owner{true}
    {}

    ~Xref()
    {
        if(owned) delete elem;
    }
    //...

private:
    int index;
    T *elem;
    bool owned;
};

string x{"There and back again"};

Xref<string> r1{7, "Here"}; //调用Xref(int i, T &&r)
Xref<string> r2{9, x} //调用Xref(int i, T& r)
Xref<string> r3{3, new string{"There"}}; //调用Xref(int i, T *p)

template<typename T>
    T&& std::forward(typename remove_reference<T>::type &t) noexcept;

template<typename T>
    T&& std::forward(typename remove_reference<T>::type &&t) noexcept;

template<typename TT, typename A>
unique_ptr<TT> make_unique(int i, A&& a)
{
    return unique_ptr<TT>{new TT{i, forward<A>(a)}};
}

函数模板重载

template<typename T>
    T sqrt<T>;
template<typename T>
    complex<T> sqrt(complex<T>);
double sqrt(double);

void f(complex<double> z)
{
    sqrt(2); //sqrt<int>(int)
    sqrt(2.0); //sqrt(double)
    sqrt(z): //sqrt<double>(complex<double>)
}
  1. 找到参与重载解析的所有函数模板特例化版本
  2. 如果两个函数模板都可以调用,且其中一个比另一个更特殊化,则接下的步骤只考虑最特殊化的版本
  3. 对前两个步骤还留在候选集中的函数和所有候选普通函数一起进行重载解析
  4. 如果一个普通函数和一个特例化版本匹配得一样好,那么优先选择普通函数,方法与普通函数重载解析相同
  5. 如果没有发现任何匹配,则调用错误;如果得到多个一样好的匹配,则调用有二义性,也是一个错误

可以通过显式限定来消除二义性,如

template<typename T>
T max(T, T)
void f()
{
    max<int>('a', 1);
    max<double>(2.7, 4);
}

实参代入失败

template<typename Iter>
typename Iter::value_type mean(Iter first, Iter last); //1号

template<typename T>
T mean(T*, T*); //2号

void f(vector<int> &v, int *p, int n)
{
    auto x = mean(v.begin(), v.end); //正确:调用1号
    auto y = mean(p, p + n); //正确:调用2号
}

如果我们未给出2号mean(),则编译器会为mean(p, p + n)调用1号版本,我们就会得到一个实例化错误

重载和派生
重载解析规则保证函数模板能完美第和继承机制结合使用

template<typename T>
    class B {/*...*/};
template<typename T>
    class D: public B<T> {/*...*/};

template<typename T> void f(B<T>*);

void g(B<int> *pb, D<int> *pd)
{
    f(pb);
    f(pd); //f<int>(static_cast<B<int>*>(pd))
}

重载和非推断的参数

template<typename T, typename C>
T get_nth(C &p, int n);

struct Index {
    operator int();
    //...
};

void f(vector<int> &v, short s, Index i)
{
    int i1 = get_nth<int>(v, 2); //严格匹配
    int i2 = get_nth<int>(v, s); //short到int的标准类型转换
    int i3 = get_nth<int>(v, i); //用户自定义类型转换: Index到int
}

模板别名

template<typename T, typename Allocator = allocator<T>> vector;
using Cvec = vector<char>;

template<int>
struct int_exact_traits{
    using type = int;
};

template<>
struct int_exact_traits<8>{
    using type = char;
};

template<>
struct int_exact_traits<16>{
    using type = short;
};

template<int N>
using int_exact = typename int_exact_traits<N>::type;

int_exact<8> a = 7;

源码组织

主要有两种
1. 在一个编译单元中,使用模板前包含其定义。
2. 在一个编译单元中,使用模板前只包含其声明。在编译单元中稍后的位置包含模板定义(可能在使用之后)。
3. (不支持)在一个编译单元中,使用模板前只包含其声明。在其他编译单元定义模板。