C++ 学习笔记之(16)-模板与泛型编程

C++ 学习笔记之(16)-模板与泛型编程

面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于OOP能处理类型在程序运行之前都未知的情况;而在泛型编程汇总,编译时即可获知类型。

定义模板

函数模板

函数模板就是公式,可用来生成针对特定类型的函数版本。

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

compare(1, 0);  // T 为 int, 实例化出 int compare(const int&, const int&)
template<typename T, class U> clac(const T&, const U&);
  • 非类型模板参数表示值,而非类型。可用特定的类型名来指定。非类型参数的模板实参必须是常量表达式

    // 比较不同长度的字符串字面值
    template<unsigned N, unsigned M>
    int compare(const char (&p1)[N], const char (&p2)[M])
    {
      return strcmp(p1, p2);
    }
    // 实例化出int compare(const char (&p1)[3], cosnt char (&p2)[4]),编译器会在字符串字面值常量末尾插入一个空字符
    compare("hi", "hello");  
  • 函数模板可声明为inlineconstexpr的,说明符放在模板参数列表之后,返回类型之前

    template<typename T> inline T min(const T&, const T&);
  • 函数模板和类模板成员函数的定义通常放在头文件中,且模板直到实例化时才会生成代码

类模板

类模板是用来生成类的蓝图的。与函数模板不同,编译器不能为类模板推断参数类型

template <typename T> class Blob{ /* ... */ };
Bolb<int> ia = {0}; // 保存 int 的 Blob,实例化出 template <> class Blob<int> { /* ... */ };
  • 类模板的成员函数在类外定义时,须以template开始,后接类模板参数列表

    ret-type StrBlob::member-name(param-list)  // 成员函数类外定义时
    template <typename T>
    ret-value Blob<T>::member-name(param-list)  // 类模板成员函数类模板外定义时
  • 默认情况下,对一个实例化了的类模板,其成员只有在使用时才被实例化

  • 在类模板作用域内,可以直接使用模板名而不提供模板实参。而类模板外定义成员时,必须记住,遇到类名才表示进入类的作用域。

    template <typename T> class BlobPtr{
    public:
        BlobPtr& operator++();  // 编译器处理时相当于处理 BlobPtr<T>& operator++();
    };
    template <typename T>
    BlobPtr<T> BlobPtr::operator--();  // 返回类型在作用域外,故需要指定模板参数
  • 类与友元各自是否是模板相互无关。若一个类模板包含一个非模板友元,则友元可以访问所有模板实例。若友元自身是模板,类可以授权所有友元模板实例,也可只授权给特定实例

  • 新标准可为类模板定义类型别名

    template<typename T> using twin = pair<T, T>;
    twin<string> authors;  // authors 是一个 pair<string, string>
  • 类模板可声明static成员, 所有实例化的类都共享相同的static成员。要分清模板类、类以及类的对象

模板参数

  • 模板参数会隐藏外层作用域中声明的相同名字

  • 模板内不能重用模板参数名

    typdef double A;
    template <typename A, typename B> void f(A a, B b)
    {
      A tmp = a;  // tmp 的类型为模板参数 A 的类型,而非 double
        double B;  // 错误:重声明模板参数 B
    }
  • 模板函数声明必须包含模板参数,且名字不必与定义中相同

  • 由于作用域运算符::可用来访问static成员和类型成员。故对于模板代码来说,无法确定访问的是名字还是类型。默认情况下,C++语言假定通过作用域运算符访问的名字而不是类型。可通过使用关键字typename显示告诉编译器改名字为一个类型

    // 返回 T 的 value_type 成员
    template <typename T>
    typename T::value_type top(const T& c)
    {
      if (!c.empty())
            return c.back();
        else:
            return typename T::valye_type();  // 若`C`为空,则返回一个值初始化的元素
    }
  • 默认模板实参(default template argument):新标准可以为函数和类模板提供默认实参

    // compare 有一个默认模板实参 less<T> 和一个默认函数实参 F()
    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 <class T = int> class Numbers{ /* ... */ }  // T 默认为 int
    Numbers<> ap;  // 空 <> 表示希望使用默认类型
    NUmbers<long double> ldp;

成员模板

一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数,这种成员被称为 成员模板(member function, 成员模板不能使虚函数

class DebugDelete{
public:
    DebugDelete(std::ostream &s = std::cerr): os(s) {}
    template <typename T> void operator()(T *p) const
        { os << "Deleteing unique_ptr" << std::endl; delete p;}
private:
    std::ostream &os;
};
double *p = new double;
DebugDelete d;  // 可想 delete 表达式一样使用的对象
d(p);  // 调用 DebugDelete::operator()(double *), 释放 p
  • 类模板与其成员模板有各自独立的模板参数,但类模板外定义成员模板时,类模板的参数列表在前

    template <typename T> class Blob{
      template <typename It> Blob(It b, It e);
        // ...
    }
    // 类模板外定义
    template <typename T> 
    template <typename It> Blob<T>::Blob(It, It e) { /* ... */ }

控制实例化

当模板被使用时才会进行实例化的特性会导致问题,即多个文件中实例化相同的模板产生严重的额外开销。新标注可通过 显示实例化(explicit instantiation)解决

  • 使用extern声明实例,且其中模板参数已被替换为模板实参, extern表示程序其他位置已有该实例的定义
  • 一个类模板的实例化会实例化该模板的所有成员,包括内联的成员函数

效率与灵活性

  • shared_ptr在运行时绑定删除器, 是用户重载删除器更为方便
  • unique_ptr在编译时绑定删除器, 避免了间接调用删除器的u运行时开销

模板实参推断

从函数实参来确定模板实参的过程被称为 模板实参推断(template argument deduction)

类型转换与模板类型参数

  • 将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有以下两种

    • 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*), 将 s1 转换为 const 是允许的
    int a[10], b[42];
    fobj(a, b);  // 调用 f(int *, int *);
    fref(a, b);  // 错误:数组类型不匹配
  • 如果函数参数类型不是模板参数,则对实参进行正常的类型转换

    template <typename T> ostream &print(ostream &os, const T &obj) { return os << obj; }
    ofstream f("output");
    print(f, 10);  // 使用 print(ostream&, int); 将 f 转换为 ostream&

函数模板显示实参

某些情况下,编译器无法推断模板实参的类型,或希望用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,上述两种情况经常出现。

// 编译器无法推断T1, 它未出现在函数参数列表中
template <typename T1, typename T2, typename T3> T1 sum(T2, T3);  
// 糟糕的设计:用户必须指定所有三个模板参数
template <typename T1, typename T2, typename T3> T3 alternative_sum(T2, T1);

//T1显示指定,T2 T3 由函数参数类型推断而来
auto val3 = sum<long long>(i, lng);  // long long sum(int, long), 
  • 对于模板类型参数显示指定了的函数实参,可进行正常的类型转换

    template <typename T> int compare(const T &v1, const T &v2);
    long lng;
    compare<long>(lng, 1024);  //  正确:实例化 compare(long, long);

尾置返回类型与类型转换

为了获得元素类型,可使用标准库的 类型转换(type transformation)模板,定义在type_traits

standard_type_transformation_template

// 尾置返回允许我们在参数列表之后表明返回类型,传入序列,返回序列元素引用
template <typename It> auto fcn(It beg, It end)->decltype(*beg) 
{ 
    return *beg; // 返回序列中一个元素的引用
}
// 使用 typename 告知编译器 type 表示一个类型,传入 序列,返回序列元素类型
template <typename It>
auto fcn2(It beg, It end)->typename remove_reference<decltype (*beg)>::type
{
    return *beg;  // 返回序列中一个元素的拷贝
}

函数指针和实参推断

当函数参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值

template <typename T> int compare(const T&, const T&);
// pf1 指向实例 int compare(const int&, const int&), T 的模板实参类型为 int
int (*pf1)(const int &, const int &) = compare;
// func 的重载版本;每个版本接受一个不同的函数指针类型
void func(int (*)(const string&, const string&));
void func(int (*)(const int&, const int&));
func(compare);  // 错误:使用 compare 的哪个实例?
func(compare<int>);  // 正确:显示指定实例化版本 compare(const int&, const int&)

模板实参推断和引用

从函数调用中推断类型有两个规则

  • 编译器会应用正常的引用板顶规则
  • const是底层的,不是顶层的

从左值引用函数参数推断类型

  • 当函数参数是模板类型参数的一个普通(左值)引用时(即,形如T&),只能传递给它一个左值,若实参是const,则T被推断为const类型

    template <typename T> void f1(T&);  // 实参必须是一个左值
    // 对 f1 的调用使用实参所引用的类型作为模板参数类型
    f1(i);  // i 是一个 int, 模板参数类型 T 是 int
    f1(5);  // 错误:传递给一个 & 参数的实参必须是一个左值
  • 若函数参数类型为const T&, 可传递任何类型的实参,const为函数参数类型的一部分,故函数实参的const属性被忽略,且const不是模板参数类型的一部分

    template <typename T> void f2(const T&);  // 可以接受一个右值
    // f2 中的参数是 const &, 实参中的 const 是无关的。下列调用,函数参数都被推断为 const int&
    f2(i);  // i 是一个 int; 模板参数 T 为 int
    f2(ci);  // ci 是一个 const int, 但模板参数 T 为 int
    f2(5);  // 一个 const & 参数可以绑定到一个右值, T 为 int

从右值引用函数参数推断类型

当函数参数为右值引用时,可传递右值。T 的类型为右值实参类型

template <typename T> void f3(T&&);
f3(42);  // 实参为 int 型右值,模板参数 T 为 int

引用折叠和右值引用参数

  • 若传递左值给函数的右值引用参数,且右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型(T&)

  • 引用的引用:被折叠成普通引用

    • X& &X& &&X&& &都折叠成类型X&
    • 类型X&& &&折叠成X&&
  • 引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数

    f3(i);  // 实参是一个左值; 模板参数 T 是 int &
    f3(ci);  // 实参是一个最值; 模板参数 T 是一个 const int&
  • 右值引用常用于模板转发实参或模板重载

理解 std::move

标准库中move函数的定义

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

string s1("hi"), s2;
s2 = std::move(string("bye"));  // 正确:实例化 string&& move(string &&t)
s2 = std::move(s1);  // 正确:实例化 string&& move(string &t)
  • 函数参数T&&为指向模板类型参数的右值引用,通过引用折叠,可匹配任何类型实参
  • 通过static_cast显示地将左值转换为右值引用

转发

某些函数需要将一个或多个实参连同类型不变地转发给其它函数,在此情况下,需要保持被转发实参的所有性质,包括实参类型是否为const以及实参左值右值属性

  • 通过将函数参数定义为指向模板类型参数的右值引用,即可保持对应实参的所有类型信息
  • 通过引用参数保持const属性,因为引用中const是底层的
  • 若函数参数定义为T1 &&T2&&, 通过引用折叠,可保持实参的左值右值属性
  • 当用于一个指向模板参数类型的右值引用函数参数(T&&)时,forward会保持实参类型的所有细节
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2) 
{
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

重载与模板

如果有多个函数提供同样好的匹配

  • 如果同样好的函数中只有一个是非模板函数,则选择此函数
  • 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,选择此模板
  • 否则,此调用有歧义

可变参数模板

可变参数模板(variadic template)即一个接受可变数目参数的模板函数或模板类,可变数目的参数被称为 参数包(parameter packet), 存在两种参数包

  • 模板参数包(template parameter packet):表示零个或多个模板参数
  • 函数参数包(function parameter packet):表示零个或多个函数参数
// Args 是一个模板参数包,rest 是一个函数参数包, Args/rest 表示零个或多个模板类型参数/函数参数
template <typename T, typename... Args> void foo(const T &t, const Args& ... rest)
{
    cout << sizeof...(Args) << endl;  // 类型参数的数目
    cout << sizeof...(rest) << endl;  // 函数参数的数目
}
int i = 0; double d = 3.14; string s = "how now brown cow";
// 包中有三个参数,实例化出void foo(cosnt int&, const string&, const int &, const double &);
foor(i, s, 42, d);  

编写可变参数函数模板

当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归

template <typename T> ostream &print(ostream &os, const T &t)
{
    return os << t;  // 包中最后一个元素之后不打印分隔符
}

template <typename T, typename... Args> 
ostream &print(ostream &os, const T &t, const Args... rest)
{
    os << t << ", ";
    return print(os, rest...);
};
print(cout, i, s, 42);  // 包中有两个参数,输出 0, how now brown cow, 42

包扩展

对参数包,还可扩展(expand), 扩展包时,需提供用于每个扩展元素的模式(pattern)。扩展包即将包分解为构成的元素,对每个元素应用模式,获得扩展后的列表,通过在模式右边添加省略号...触发扩展操作

template <typename... Args> ostream &errorMsg(ostream &os, cosnt Args&... rest)
{
    // print(os, debug_rep(al), debug_rep(a2), ..., debug_rep(an))
    return print(os, debug_rep(rest)...);
}
errorMsg(cerr, fcnName, code.num(), otherData, "other", item); // 等价于下式
print(cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(otherData),
     debug_rep("otherData"), debug_rep(item));
// 将包传递给debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...));  // 错误:此调用无匹配函数
  • print调用使用了模式debug_rep(rest), 表示对函数参数包rest中每个元素调用debug_rep
  • 扩展中的模式会独立地应用于包中的每个元素

转发参数包

在新标准下,可组合使用可变参数模板与forward机制来编写函数,实现将实参不变地传递给其它函数

template <typename... Args> void fun(Args&&... args)  // 将 Args 扩展为一个右值引用的列表
{
    // work 的实参即扩展 Args 又扩展 args
    work(std::forward<Args>(args)...);
}

模板特例化

通用模板的定义可能不适合特定类型,故此时可定义类或函数模板的一个特例化版本

// 第一个版本:可以比较任意两个类型
template <typename T> int compare(const T&, const T&);
// 第二个版本处理字符串字面常量,处理的是数组,不是指针,指针无法转换为数组引用
template <size_t N, size_t M> int compare(const char (&)[N], const char (&)[M]);
// compare 的特例化版本,处理字符数组的指针
template <> int compare(const char *const &p1, const char *const &p2);

const char *p1 = "hi", *p2 = "mom";
compare(p1, p2);  // 调用第一个模板
compare("hi", "mom");  // 调用有两个非类型参数的版本,若存在第三个函数,即模板特例化函数,选用函数3
  • 特例化的本质是实例化一个模板,而非重载它。故,特例化不影响函数匹配
  • 模板及其特例化版本应该声明在同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本

结语

  • 模板是C++语言与众不同的特性,也是标准库的基础。一个模板就是一个编译器用来生成特定类型或函数的蓝图。生成特定类或函数的过程称为实例化。我们只编写一次模板,就可以将其用于多种类型和值,编译器会为每种类型和值进行模板实例化。
  • 我们既可以定义函数模板,也可以定义类模板。标准库算法都是函数模板,标准库容器都是类模板
  • 显示模板实参允许我们固定一个或多个模板参数的类型或值。对于指定了显示模板实参的模板参数,可以应用正常的类型转换
  • 一个模板特例化就是一个用户提供的模板实例,它将一个或多个模板参数绑定到特定类型或值上。当我们不能(或不希望)将模板定义用于某些特定类型时,特例化非常有用
  • 最新C++标准的一个主要部分是可变参数模板。一个可变参数模板可以接受数目和类型可变的参数。可变参数模板允许我们编写像容器的emplace成员和标准库make_shared函数这样的函数,实现将实参传递给对象的构造函数

猜你喜欢

转载自blog.csdn.net/u011221820/article/details/80231635