【C++学习】类设计工具:模板与泛型编程(总结C++primer第十六章)

模板与泛型编程

  • 在泛型编程中,在编译时就能获知类型;

  • 模板是C++中泛型编程的基础,一个模板就是一个创建类或函数的蓝图或者公式。

  • 标准库算法都是函数模板,标准库容器都是类模板

定义模板

  • 函数模板

    • 一个函数模板就是一个公式,可以用来生成针对特定类型的函数版本;

    • 模板定义

      • 以关键字 template 开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用<>包围起来,模板参数列表不能为空:
      template<typename T>
          int compare(const T &v1,const T &v2) 
          {
              if (v1 < v2) return -1;
              if (v1 > v2) return 1;
              return 0;
          }          
      
      • 模板参数表示在类或函数定义中用到的类型或值,当使用模板时,显式地或隐式地指定模板参数,将其绑定到模板参数上。
    • 实例化函数模板:编译器使用推断出的模板参数来实例化出一个特定版本的函数。

    • 模板类型参数

      • 一般来说,可以将类型参数看作类型说明符,就像内置类型和类类型说明符一样使用,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换;

      • 类型参数前必须使用关键字 class 或 typename;

        template<typename T> T foo(T* p)
        {
        	T tmp = *p;	//tmp的类型将是指针p指向的类型
        	//...
        	return tmp;
        }
        
    • 非类型模板参数

      • 一个非类型参数表示一个值而非一个类型,通过一个特定的类型名指定非类型参数;

      • 非类型参数可以是整形、指针或左值引用;

      • 非类型模板参数的实参必须是常量表达式,从而允许编译器在编译时实例化模板;

        template<unsigned N, unsigned M>
        int compare(const char (&p1)[N], const char&p2)[M])
        {
        	return strcmp(p1, p2);
        }
        
        //当调用这个版本的 compare 时
        compare("hi", "mom");
        //会实例化出如下版本
        int compare(const char (&p1)[3], const char&p2)[4]);
        
    • inline 和 constexpr 的函数模板

      • 函数模板可以声明为 inline 或 constexpr 的,如同非模板函数一样,inline 或 constexpr 说明符放在模板参数列表之后,返回类型之前

        //正确
        template <typename T> inline T min(const T&, const T&);
        //错误:inline说明符的位置不正确
        inline template<typename T> T min(const T&, const T&);
        
    • 模板程序需要尽量减少对类型的依赖

      • 模板中的函数参数设为const的引用,避免一些类无法进行拷贝;
      • 可以使用less<T>来实例化,而不是使用<或>
    • 模板编译

      • 当编译器遇到一个模板定义时,它并不生成代码,只有当实例化出模板的一个特定版本时,编译器才会生成代码
      • 一般将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中;
      • 函数模板和类模板成员函数的定义通常放在头文件中
  • 类模板

    • 编译器不能为类模板推断模板参数类型,为了使用类模板(和我们以前使用模板一样),必须在模板名后的尖括号中提供用来代替模板参数的模板实参列表;

    • 类模板定义

      template <typename T> 
          class Blob {
          public:
              typedef T value_type;
              // 为什么需要加上typename
              typedef typename vector<T>::size_type size_type;
              // 构造函数
              Blob();
              Blob(initializer_list<T> il);
              // 添加删除元素。
              void push_back(const T &t) {
                  data->push_back(t);
              }
              void push_back(T &&t) {
                  data->push_back(std::move(t));
              }
              void pop_back();
              T& back();
              T& operator[](size_type i);
          private:
              Shared_ptr<vector<T>> data;
              void check(size_type i, const string &msg) const;
              
          };
      
    • 实例化模板

      • 当我们使用一个类模板时,必须提供额外信息即显式模板实参列表,它们被绑定到模板参数,编译器通过使用这些模板实参实例化出特定的类;

        //实例化类模板
        Blob<int> ia;//空的Blob
        Blob<int> ia2 = {0,1,2,3,4};//有5个元素的Blob
        // 此时编译器会实例化出一个与下面定义等价的类。
        template<>
        class Blob<int> {
                ...//所有的T都使用int代替。
            };
        }
        
      • 一个类模板的每个实例都形成一个独立的类

    • 类模板的成员函数

      • 可以在类模板内部也可以在类模板外部定义其成员函数,定义在类模板内的成员函数被隐式声明为内联函数;

      • 定义在类模板之外的成员函数必须以关键字 template 开始,后接类模板参数列表:

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

    • 在类代码内简化模板类名的使用

      • 当使用一个类模板类型时,必须提供模板实参,但有一例外,在类模板自己的作用域中,可以直接使用模板名而不提供实参:

        template<typename T> class BlobPtr{
        public:
        	//...
        	//返回类型为BlobPtr&,而非 BlobPtr<T>&
        	BlobPtr& operator++();
        	BlobPtr& operator--();
        private:
        	//...
        }
        
    • 在类模板外使用类模板名

      • 当在类模板外定义其成员时,直到遇到类名才表示进入类的作用域;

        //返回类型位于类的作用域之外,因此必须指出返回类型是一个实例化的BlobPtr
        template<typename T>
        BlobPtr<T> BlobPtr<T>::operator++(int)
        {
        	//已经进入类的作用域,因此在定义ret时无须重复模板实参
        	//如果不提供模板实参,则编译器假定我们使用的类型与成员实例化所用类型完全一致
        	BlobPtr ret = *this;	
        	++*this;		
        	return ret;		
        }
        
    • 类模板和友元

      • 一个类模板中包含一个非模板友元,则友元被授权可以访问所有模板实例;
      • 如果友元是模板,类可以授权给所有友元实例,也可以授权给部分实例。
    • 一对一友好关系

      • 友元的声明用 Blob 的模板形参作为它们自己的模板实参,因此友好关系被限定在用相同类型实例化的 Blob 与 BlobPtr 相等运算符之间:

        //前置声明,在Blob中声明友元所需要的
        template <typename> class BlobPtr;
        template <typename> class Blob;
        template <typename T>
        	bool operator==(const Blob<T>&, const Blob<T>&);
        
        template <typename T> class Blob{
        	//每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
        	friend class BlobPtr<T>;
        	friend bool operator==<T>
        			(const Blob<T>&, const Blob<T>&);
        	//...(其他成员定义与12.1.1相同)
        }
        
    • 通用和特定的模板友好关系

      • 一个类可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元;

      • 为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数:

        template <typename T> class Pal;
        class C{
        	friend class Pal<C>;	//用类C实例化的Pal是C的一个友元
        	//Pal2 的所有实例都是 C 的友元,这种情况无须前置声明
        	template <typename T> friend class Pal2;
        };
        
    • 令模板自己的类型参数成为友元

      template <typename Type> class Bar{
      friend Type;	//将访问权限授予用来实例化Bar的类型
      	//...
      }
      
    • 类模板的 static 成员

      • 模板类的每个 static 数据成员必须有且仅有一个定义,但是,类模板的每个实例都有各自独享的 static 对象,因此,与定义模板的成员函数类似,将 static 数据成员也定义为模板

        template <typename T> class Foo{
        public:
        	static std:size_t count() { return ctr; }
        	//其他接口成员
        private:
        	static std::size_t ctr;
        	//其他实现成员
        };
        
        template <typename T>
        size_t Foo<T>::ctr = 0;	//定义并初始化ctr
        
      • 与非模板静态成员相同,可以通过类类型对象来访问一个类模板的 static 成员,也可以使用作用域运算符直接访问成员:

        Foo<int> fi;					//实例化Foo<int>类和static数据成员ctr
        auto ct = Foo<int>::count();	//实例化Foo<int>::count
        ct = fi.count();				//使用Foo<int>::count
        ct = Foo::count();				//错误:使用哪个模板实例的count
        
  • 模板参数

    • 类似函数参数的名字,一个模板参数的名字没有什么内在含义,我们通常将类型参数命名为 T,但实际上我们可以使用任何名字

    • 模板参数与作用域

      • 一个模板参数名的可用范围是其声明之后,至模板声明或定义结束之前;

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

      • 与大多数其他上下文不同,在模板内不能重用模板参数名;

        typedef 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关键字告诉编译器该名字是一个类型而非 static 成员;

      • T::size_type * p;//编译器不知道这是定义一个变量还是一个乘法。
        
        template <typename T>
        typename T::value_type top(const T& c)
        {
        	if(!c.empty())
        		return c.back();
        	else
        		return typename T::value_type();//表示值初始化的一个变量
        }
        
    • 默认模板实参:与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参

    • 模板默认实参与类模板

      • 无论何时使用一个类模板,都必须在模板名后接上尖括号;
      • 尖括号指出类必须从一个模板实例化而来;
      • 若一个类模板提供了默认实参,且用户希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对。
  • 成员模板

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

    • 普通(非模板)类的成员模板

      class DebugDelete 
      {
          public:
              DebugDelete(ostream &s = cerr) : os(s) {}
              template <typename T>
              void operator()(T *p) const {
                  os << "deleting unique_ptr" << endl;
                  delete p;
              }
          private:
              ostream &os;
      };
      
    • 类模板的成员模板

      • 类和成员各自有自己的,独立的模板参数:
      // 为模板定义了一个构造函数,我们希望这个构造函数可以接受任意类型的迭代器;于是它就别定义为模板函数。
      // 注意到类和函数的模板参数是相互独立的。
      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(make_shared<vector<T>>)(b, e) {}
      
    • 实例化与成员模板

      • 为了实例化一个类模板的成员模型,必须同时提供类和函数模板的实参

      • 我们在哪个对象上调用成员模板,编译器就根据该对象的类型来推断类模板参数的实参

        int ia[] = {0,1,2,3,4,5,6};
        Blob<int> a1(begin(ia), end(ia));
        

        注:定义 a1 时,显式地指出编译器应该实例化一个 int 版本的 Blob;构造函数自己的类型参数则通过 begin(ia) 和 end(ia) 的类型来推断,因此,a1 的定义实例化了该版本:Blob::Blob(int, int)。

  • 控制实例化

    • 由于模板使用时才会进行实例化,如果在多个独立编译的源文件中都使用了相同的模板,并且提供了一样的模板参数时,每一个源文件就都会有一个该模板的实例造成额外开销。

      • 解决办法,显式实例化:
      extern template declaration;//实例化声明
      template declaration;//实例化定义。
      
      // 注意所有的模板参数已经替换为为模板实参。
      extern template class Blob<string>;//声明
      template int compare(const int &, const int &);//定义
      
      • 当编译器遇到extern声明时,它不会在本文件中生成实例化代码;将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义);
      • 对于给定的一个实例化版本,可能有多个extern声明,但是必须只有一个定义;
      • 编译器使用一个模板时会自动对其实例化,因此extern声明必须出现在任何使用此实例化版本代码之前。
    • 实例化定义会实例化所有成员,包括内联的成员函数。

  • 效率和灵活性

    • unique_ptrshared_ptr的设计:
      • 都是模板,可以在运行时改变存储的类型;
      • 对于unique_ptr删除器类型也是它的类型的一部分;
      • 在运行时候绑定删除器,shared_ptr的重载比较方便;在编译器时绑定删除器,避免了间接调用的额外开销,但是重载起来比较麻烦。

模板实参推断

模板实参推断:编译器确定实例化哪个函数模板的过程,编译器检查哪些使用模板参数的实参的类型,将这些类型或值绑定到模板参数,来自动实例化一个函数版本。

  • 类型转换与模板类型参数

    • 如果一个函数形参的类型使用了模板类型参数,则采用特殊的初始化规则,只有有限的几种类型转换会自动地应用于这些实参,编译器通常不是对实参进行类型转换,而是生成一个新的模板实例;

    • 顶层 const 无论是在形参还是实参中,都会被忽略;

    • 应用于函数模板的类型转换包括以下两项(对实参进行转换)

      • const 转换:可以将一个非 const 对象的引用(或指针)传递给一个 const 的引用(或指针)实参;
      • 数组或函数指针转换:若函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换,一个数组实参可以转换为一个指向其首元素的指针,一个函数实参可以转换为一个该函数类型的指针;
      • 其他类型转换,如算数转换、派生类向基类的转换、用户定义的转换,都不能应用于函数模板。
    • 使用相同模板参数类型的函数实参

      • 当一个模板类型参数用作多个函数形参的类型时,实参必须有相同的类型。如果推断出来的类型不匹配,调用iu是错误的

        long lhg;
        compare(lng, 1024);//错误,不能实例化compare(long, int)
            
        //如果希望函数实参可以进行正常的类型转换,可以将函数模板定义为两个类型参数。
        template <typename A, typename B>
         int flexibleCompare(const A &v1, const B &v2) {
            if...
        } 
        // 使用。
        long lng;
        flexibleCompare(lng, 1024);//使用的是flexibleCompare(long, int)
        
      • 如果函数参数类型不是模板参数,则对实参进行正常的类型转换。

  • 函数模板的显式实参(显式实例化)

    • 指定显式模板实参

      • 在某些情况下,编译器无法推断出模板实参的类型;

      • 或者我们希望允许用户控制模板实例化,当函数返回类型与参数列表中任何类型都不同时,这两种情况常常出现:

        template <typename T1, typename T2, typename T3>
        T1 sum(T2, T3);
        
        auto val3 = sum<long long>(i, lng);	//long long sum(int, long)
        
      • 显式模板实参按由左至右的顺序与对应模板参数匹配,只有尾部(最右)参数的显式模板实参才可以忽略:

        template <typename T1, typename T2, typename T3>
        T3 alternative_sum(T2,T1);
        // 此时我们必须为三个模板参数指定实参
        auto val3 = alternative_sum<long long>(i, lng);//错误,不能推断第三个模板参数
        auto val2 = alternative_sum<long long, int, long>(i, lng);
        
    • 如果已显示指定模板类型参数,就可以进行正常的类型转换:

      long lng;
      compare(lng, 1024);//错误,模板参数不匹配
      compare<long>(lng, 1024);//实例化compare(long, long)
      compare<int>(lng, 1024);//实例化compare(int, int)  可以进行类型转换。
      // 显式指定的参数,将会 直接替换 模板参数列表里的参数
      
  • 尾置返回类型与类型转换

    • 尾置返回允许我们在参数列表之后声明返回类型

      template <typename It>
      auto fcn(It beg, It end)->decltype(*beg)
      {
      	//处理序列
      	return *beg;	//返回序列中一个元素的引用
      }
      

      **注:**在编译器遇到参数列表之前,beg 都是不存在的,为了定义该函数, 必须使用尾置返回类型,因为尾置返回出现在参数列表之后,可以使用函数的参数。

    • 进行类型转换的标准库模板类

      • 有时候需要返回一个元素的值而不是它的引用,但是我们传入的是迭代器,对元素的类型一无所知,并且迭代器解引用只能返回元素的引用。

      • 使用标准库的类型转换模板,定义在头文件type_traits中,使用remove_reference来得到元素类型。使用,remove_reference<int &>则type成员将会是int;如果给定的是一个迭代器,decltype(*b)返回元素类型的引用类型,remove_reference::type 脱去引用,剩下元素类型本身;

        template <typename T> auto fcn2(It b, It e) -> 
        typename remove_reference(*b)::type 
        {
            //处理
            return *b;//返回一个值,元素的一个拷贝,而不是一个引用
        }
        
  • 函数指针和实参推断

    • 当使用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参;

      template <typename T> int compare(const T&, const &T);
      //pf1 指向实例 int compare(const int&, const int&)
      int (*pf1)(const int&, const int&) = compare;
      
    • 若不能从函数指针类型确定模板实参,则产生错误:

      void func(int(*)(const string&, const string&));
      void func(int(*)(const int&, const int&));
      func(compare);		//错误:无法确定使用哪个实例
      func(compare<int>);//正确,调用的是compare(int (*)(const int&, const int&));
      
  • 模板实参推断和引用

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

      • 当一个函数参数是模板类型参数的一个普通(左值)引用时,只能传递一个左值(如一个变量或一个返回引用类型的表达式),实参可以是 const 类型也可以不是;

        template <typename T> void f1(T&);
        f1(i);	//i 是一个 int;模板参数类型T是 int
        f1(ci);	//ci 是一个 const int;模板参数T是 const int
        f1(5);	//错误:实参必须是一个左值
        
      • 若一个函数参数是 const T&,可以传递任何类型实参(一个对象、一个临时对象或一个字面常量值),当函数参数本身是 const 时,T 的类型推断结果不会是一个 const 类型,因为 const 已经是函数类型的一部分:

        template <typename T> void f2(const T&);	
        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
        
    • 引用折叠和右值引用参数

      • 通常不能将一个右值引用绑定到一个左值上,但C++语言在正常绑定规则之外定义了两个例外,允许这种绑定;
      • 当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型
      • 当我们间接创建一个引用的引用,则这些引用形成了折叠,即X& &、X& &&、X&& & 都折叠成类型 X&类型X&& && 折叠成 X&&
      • 引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
      • 根据引用折叠规则,如果一个函数参数是指向模板参数类型的右值引用,则可以传递给它任意类型实参,若将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。
  • 理解 std::move

    • std::move的定义

      template <typename T>
      typename remove_reference<T>::type&& move(T&& t)
      {
      	return static_cast<typename remove_reference<T>::type&&>(t);
      }
      
    • std::move的使用

      string s1("hi!"), s2;
      s2 = std::move(string("bye!"));
      s2 = std::move(s1);
      
      • 在第一个赋值中,传递给 move 的实参是 string 的构造函数的右值结果,推断出T的类型为 string,move 的返回类型为 string&&,因此,这个调用实例化 move,即函数 string&& move(string &&t);
      • 在第二个赋值中,传递给 move 实参的是一个左值,推断出的 T 的类型为 string&,move 返回类型仍是 string&&,因此,这个调用实例化move,即string&& move(string &t)。
    • 可以使用static_cast 显式地将一个左值转换为一个右值引用。

  • 转发

    • 某些函数需要将一个或多个实参连同类型不变地转发给其他函数,此时需要保持被转发实参的所有性质,包括实参类型是否是 const 的以及实参是左值还是右值。

    • 定义能保持类型信息的函数参数:如果一个函数参数是指向模板类型参数的右值引用,它对应的实参的 const 属性和左值/右值属性将得到保持;(注意代码中的“问题”)

      void f(int v1, int &v2) 
      {
          cout << v1 << " " << ++v2 << endl;
      }
      
      template <typename F, typename T1, typename T2>
      void flip2(F f, T1 &&t1, T2 &&t2) {
          f(t2, t1);
      }
      
      // 问题,如果,我们的可调用对象的形参是右值引用,他不可以接受左值。如何解决,即:
      void g(int &&i, int &j) 
      {
          cout << i << " " << j << endl;
      }
      
      flip2(g, i, 42) //错误,不能从一个左值实例化int&&
      //传递给g的将是flip2中名为t2的参数,函数参数是左值表达式
      
    • 在调用中使用 std::forward 保持类型信息

      • 定义在头文件utility中,forward返回该显示实参类型的的右值引用;
      • 通常情况下,使用forward传递那些定义为模板类型参数的右值引用的函数参数,通过返回类型上的引用折叠,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));
          }
          // 如果调用,flip(g, i, 42);
          // i将会以int &的形式传递给g
          // 42将会以int &&的形式传递给g
          // 以forward的返回值的类型传递给函数。
      

重载与函数模板

  • 函数模板可以被另一个模板或一个普通非模板函数重载,名字相同的函数必须有不同数量或类型的参数;
  • 匹配规则:如果恰有一个函数提供比任何其他函数更好的匹配,则选择此函数,但是,如果有多个函数提供同样好的匹配,则:
    • 如果同样好的函数中只有一个是非模板函数,则选择此函数;
    • 如果同样好的函数中没有非模板,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板;
    • 否则,此调用有歧义。
  • 缺少声明可能导致程序行为异常:在定义任何函数之前,需要声明所有重载的函数版本,这样就不必担心编译器由于未遇到希望调用的函数而实例化一个并非你所需的版本。

可变参数模板

  • 可变参数模板:即一个接受可变数目参数的模板函数模板类,可变数目的参数被称为参数包,存在两种参数包:

    • 模板参数包:表示零个或多个模板参数;

    • 函数参数包,表示零个或多个函数参数;

    • 通过用一个省略号来指出一个模板参数函数参数表示一个包,在一个模板参数列表中,
      class… 或 typename…指出接下来表示零个或多个类型的列表

    • 在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。

      // Args是一个模板参数包,rest是一个函数参数包
      // Args表示零个或则多个模板参数列表
      // rest表示零个零个或者多个函数参数。
      template <typename T, typename... Args>
      void foo(const T &t, const Args&... rest);
      // 声明了foo是一个可变参数函数模板,它有一个名为T的的类型参数,和一个名为Args的模板参数包,这个包表示零个或者多个额外的类型参数。
      // foo函数参数列表包含一个const&的参数,指向T类型,还包含一个名为rest的函数参数包。
      
      // 使用,
      int i = 0;
      double d = 2.22;
      string s = "how new";
      foo(i, s, 24, d);//包含三个参数
      foo(s, 42, "hi");//包含两个参数
      foo(s, d);//包含一个参数
      foo("hi");//空包
      // 编译器实例化的四个版本
      void foo(const int&, const string&, const int&, const double&);
      void foo(const string&, const int&, const char[3]&);
      void foo(const string&, const double&);
      void foo(const char[3]&);
      // 每一个实例,T的类型都是从第一个实参推断出来的,剩下的实参提供给函数额外的类型参数。
      // 注意到保留了const属性和&属性
      
    • sizeof…运算符:返回包里面的元素数目。

      template<typename...Args> void g(Args...args){
      	cout << sizeof...(Args) << endl;	//类型参数的数目
      	cout << sizeof...(args) << endl;	//函数参数的数目
      }
      
  • 编写可变参数函数模板

    • 可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后用剩余实参调用自身;

    • 为了终止递归,还需要定义一个非可变参数的函数。

      //用来终止递归并打印最后一个元素的函数,其实可变参数模板也能匹配,但是非可变模板更特例化,因此编译器会选择非可变参数版本
      //此函数必须在可变参数版本的print定义之前声明
      template<typename T>
      ostream &print(ostream &os, const T &t)
      {
      	return os << t;
      }
      //包中除了最后一个元素之外的其他元素都会调用这个版本的 print
      template<typename T, typename...Args>
      ostream &print(ostream &os, const T &t, const Args&...rest)
      {
      	os << t << ",";			  //打印第一个实参
      	//递归调用,打印其他实参
      	return print(os, rest...);//递归传参时,rest 的第一个参数赋给形参的 const T& t,剩下的参数继续以参数包 rest 存在,并继续递归
      }
      
    • 当定义可变参数print时,非可变参数版本的声明必须在作用域中,否则将会导致可变参数版本无线递归。

  • 包扩展

    • 对于一个参数包,我们可以获取其大小,还能对他做的唯一一件事情就是扩展;

    • 当我们扩展一个包时,我们还有提供用于每一个扩展元素的模式,扩展一个包就是将他分解为构成的元素,对每一个元素应用模式,获取扩展后的元素。

      template <typename T, typename... Args>
      ostream& print(ostream &os, const T &t, const Args&... rest) //扩展了Args
      { 
          os << t << ",";		
          return print(os, rest...);//扩展rest
      }
      // 第一个扩展扩展模板参数包,为print生成函数参数列表
      // 第二扩展为实参,为print调用生成实参列表。
      
      // 在Args的扩展中,编译器将const Args&的模式应用到模板参数包Args中的每一个元素。因此,此模式的扩展结果就是一个逗号分割的零个或者多个类型的列表
      // 每一个类型都形如,const type&。
      print(cout, i, s, 42);//一个是string一个是int
      
      // 最后实例化的是
      ostream& print(stream &, const int &, const string &, const int &);
      
      // 第二个扩展,模式就是函数参数包的名字,此扩展就扩展出一个包中元素组成的,由逗号分割的列表。
      print(cout, s, 42);//等价于该形式
      
      • 扩展中的模式会独立地应用于包中的每个元素。
  • 转发参数包(不太懂)

    • 通过组合使用可变参数模板与 forward 机制来编写函数,实现将实参不变地传递给其他函数。

模板特例化

  • 当我们不能(或者不希望)使用模板版本时,可以定义类或者函数模板的一个版本特例化。

  • 定义函数模板特例化

    • 当特例化一个函数模板时,必须为原模板中的每一个模板参数提供实参;

    • 为了指出正在实例化一个模板,应该使用关键字 template 后跟一个空尖括号对(<>),指出将为原模板所有模板参数提供实参。

      template<typename T> int compare(const T&, const T&);
      
      //compare 的特殊版本,处理字符数组的指针
      template<>
      int compare(const char* const &p1, const char* const &p2)
      {
      	return strcmp(p1, p2);	
      }
      
  • 函数重载与模板特例化

    • 特例化的本质是实例化一个模板,而非重载,因此特例化不影响函数匹配;
    • 为了特例化一个模板,原模板的声明必须在作用域中,而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中;
    • 模板及其特例化版本应该声明在同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
  • 类模板的特例化

    • 可以通过打开命名空间,从而向命名空间添加成员:

      //打开std命名空间
      namespace std{
      }//关闭std命名空间
      
  • 类模板部分特例化

    • 与函数模板不同,类模板的特例化不必为所有模板参数提供实参,可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性;

    • 我们只能部分特例化类模板,而不能部分特例化函数模板

    • 一个类模板的部分特例化本身也是一个模板,使用它的用户还必须为那么特例化中未指定的模板参数提供实参。

      // 原始的,通用的版本
      template <class T> struct remove_reference 
      {
          typedef T type;
      };
          
      //部分特例化版本,将用于左值引用和右值引用
      // 左值引用
      template <class T> struct remove_reference <T&> 
      {
          typedef T type;
      };
      // 右值引用
      template <class T> struct remove_reference <T&&> 
      {
          typedef T type;
      };
      
  • 特例化成员而不是类

    • 我们可以只特例化特定成员函数而不是特例化整个模板:

      template <typename T> struct Foo{
      	Foo(const T &t = T()) : mem(t){ }
      	void Bar() {/* ... */}
      	T mem;
      	//Foo的其他成员
      };
      
      template<>			//正在特例化一个模板
      void Foo<int>::Bar()	//正在特例化Foo<int>的成员Bar
      {
      	//进行应用于int的特例化处理
      }
      
      Foo<string> fs;	//实例化Foo<string>::Foo()
      fs.Bar();		//实例化Foo<string>::Bar()
      Foo<int> fi;	//实例化Foo<int>::Foo()
      fi.Bar();		//使用我们特例化版本的Foo<int>::Bar()
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值