Skip to content

Latest commit

 

History

History
1056 lines (750 loc) · 35.1 KB

File metadata and controls

1056 lines (750 loc) · 35.1 KB

三、模板编程

在本章中,我们将涵盖以下主题:

  • 泛型编程
  • 功能模板
  • 类模板
  • 重载函数模板
  • 泛型类
  • 显式类专门化
  • 部分专业化

现在让我们开始学习泛型编程。

泛型编程

泛型编程是一种帮助您开发可重用代码或泛型算法的编程风格,这些代码或算法可以应用于各种各样的数据类型。无论何时调用通用算法,数据类型都将作为具有特殊语法的参数提供。

假设我们想写一个sort()函数,它接受一个需要按升序排序的输入数组。其次,我们需要sort()功能对intdoublecharstring数据类型进行排序。有几种方法可以解决这个问题:

  • 我们可以为每种数据类型编写四个不同的sort()函数
  • 我们还可以编写一个宏函数

嗯,这两种方法各有优缺点。第一种方法的优点是,由于intdoublecharstring数据类型有专用函数,如果提供了不正确的数据类型,编译器将能够执行类型检查。第一种方法的缺点是,我们必须编写四个不同的函数,即使所有函数的逻辑保持不变。如果在算法中发现 bug,必须在所有四个函数中分别修复;因此,需要大量的维护工作。如果我们需要支持另一种数据类型,我们最终会再编写一个函数,并且随着我们需要支持更多的数据类型,这个函数会不断增长。

第二种方法的优点是,我们可以只为所有数据类型编写一个宏。然而,一个非常令人沮丧的缺点是,编译器将无法执行类型检查,并且这种方法更容易出错,可能会带来许多意想不到的麻烦。这种方法完全违背了面向对象的编码原则。

C++ 支持使用模板的泛型编程,这有以下好处:

  • 我们只需要使用模板编写一个函数
  • 模板支持静态多态性
  • 模板提供了上述两种方法的所有优点,没有任何缺点
  • 泛型编程支持代码重用
  • 生成的代码是面向对象的
  • C++ 编译器可以在编译时执行类型检查
  • 易于维护
  • 支持多种内置和用户定义的数据类型

然而,缺点如下:

  • 不是所有的 C++ 程序员都觉得写基于模板的编码很舒服,但这只是一个开始
  • 在某些情况下,模板可能会膨胀您的代码并增加二进制占用空间,从而导致性能问题

功能模板

函数模板允许您参数化数据类型。之所以称之为泛型编程,是因为单个模板函数将支持许多内置和用户定义的数据类型。模板化函数的工作方式类似于 C 风格的宏,只是当我们在调用模板函数时提供不兼容的数据类型时,C++ 编译器会对函数进行类型检查。

用一个简单的例子更容易理解模板概念,如下所示:

#include <iostream>
#include <algorithm>
#include <iterator>
using namespace std;

template <typename T, int size>
void sort ( T input[] ) {

     for ( int i=0; i<size; ++ i) { 
         for (int j=0; j<size; ++ j) {
              if ( input[i] < input[j] )
                  swap (input[i], input[j] );
         }
     }

}

int main () {
        int a[10] = { 100, 10, 40, 20, 60, 80, 5, 50, 30, 25 };

        cout << "\nValues in the int array before sorting ..." << endl;
        copy ( a, a+10, ostream_iterator<int>( cout, "\t" ) );
        cout << endl;

        ::sort<int, 10>( a );

        cout << "\nValues in the int array after sorting ..." << endl;
        copy ( a, a+10, ostream_iterator<int>( cout, "\t" ) );
        cout << endl;

        double b[5] = { 85.6d, 76.13d, 0.012d, 1.57d, 2.56d };

        cout << "\nValues in the double array before sorting ..." << endl;
        copy ( b, b+5, ostream_iterator<double>( cout, "\t" ) );
        cout << endl;

        ::sort<double, 5>( b );

        cout << "\nValues in the double array after sorting ..." << endl;
        copy ( b, b+5, ostream_iterator<double>( cout, "\t" ) );
        cout << endl;

        string names[6] = {
               "Rishi Kumar Sahay",
               "Arun KR",
               "Arun CR",
               "Ninad",
               "Pankaj",
               "Nikita"
        };

        cout << "\nNames before sorting ..." << endl;
        copy ( names, names+6, ostream_iterator<string>( cout, "\n" ) );
        cout << endl;

        ::sort<string, 6>( names );

        cout << "\nNames after sorting ..." << endl;
        copy ( names, names+6, ostream_iterator<string>( cout, "\n" ) );
        cout << endl;

        return 0;
}

运行以下命令:

g++ main.cpp -std=c++ 17
./a.out

前面程序的输出如下:

Values in the int array before sorting ...
100  10   40   20   60   80   5   50   30   25

Values in the int array after sorting ...
5    10   20   25   30   40   50   60   80   100

Values in the double array before sorting ...
85.6d 76.13d 0.012d 1.57d 2.56d

Values in the double array after sorting ...
0.012   1.57   2.56   76.13   85.6

Names before sorting ...
Rishi Kumar Sahay
Arun KR
Arun CR
Ninad
Pankaj
Nikita

Names after sorting ...
Arun CR
Arun KR
Nikita
Ninad
Pankaj
Rich Kumar Sahay

看到仅仅一个模板函数就能完成所有的魔法,难道不是很有趣吗?是的,这就是 C++ 模板有多酷!

Are you curious to see the assembly output of a template instantiation? Use the command, g++ -S main.cpp.

代码走查

下面的代码定义了一个函数模板。关键字template <typename T, int size>告诉编译器接下来是一个函数模板:

template <typename T, int size>
void sort ( T input[] ) {

 for ( int i=0; i<size; ++ i) { 
     for (int j=0; j<size; ++ j) {
         if ( input[i] < input[j] )
             swap (input[i], input[j] );
     }
 }

}

void sort ( T input[] )行定义了一个名为sort的函数,该函数返回void,并接收类型为T的输入数组。T类型不表示任何特定的数据类型。T将在编译时实例化函数模板时推导出来。

下面的代码用一些未排序的值填充一个整数数组,并将其打印到终端:

 int a[10] = { 100, 10, 40, 20, 60, 80, 5, 50, 30, 25 };
 cout << "\nValues in the int array before sorting ..." << endl;
 copy ( a, a+10, ostream_iterator<int>( cout, "\t" ) );
 cout << endl;

下面一行将为int数据类型实例化一个函数模板实例。此时,typename T被替换,并且为int数据类型创建了一个专门的函数。sort前面的作用域解析运算符,即::sort(),确保它调用我们在全局命名空间中定义的自定义函数sort();否则,C++ 编译器将尝试调用在std namespace中定义的sort()算法,或者从任何其他名称空间调用(如果存在这样的函数的话)。<int, 10>变量告诉编译器创建一个函数的实例,用int代替typename T10表示模板函数中使用的数组的大小:

::sort<int, 10>( a );

以下几行将分别实例化支持5元素的double数组和6元素的string数组的另外两个实例:

::sort<double, 5>( b );
::sort<string, 6>( names );

如果你想知道更多关于 C++ 编译器如何实例化函数模板来支持intdoublestring的细节,你可以试试 Unix 实用程序、nmc++ filtnm Unix 实用程序将在符号表中列出符号,如下所示:

nm ./a.out | grep sort

00000000000017f1 W _Z4sortIdLi5EEvPT_
0000000000001651 W _Z4sortIiLi10EEvPT_
000000000000199b W _Z4sortINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEELi6EEvPT_

可以看到,二进制中有三个不同的重载sort函数;然而,我们只定义了一个模板函数。由于 C++ 编译器在处理函数重载时使用了错误的名称,我们很难解释这三个函数中的哪一个函数是针对intdoublestring数据类型的。

不过有一个线索:第一个功能是给double用的,第二个是给int用的,第三个是给string用的。更名功能有double_Z4sortIdLi5EEvPT_int_Z4sortIiLi10EEvPT_string_Z4sortINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEELi6EEvPT_。还有另一个很酷的 Unix 工具可以帮助你毫不费力地解释函数签名。检查c++ filt实用程序的以下输出:

c++ filt _Z4sortIdLi5EEvPT_
void sort<double, 5>(double*)

c++ filt _Z4sortIiLi10EEvPT_
void sort<int, 10>(int*)

c++ filt _Z4sortINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEELi6EEvPT_
void sort<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, 6>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)

希望在使用 C++ 模板时,您会发现这些实用程序很有用。我相信这些工具和技术将帮助您调试任何 C++ 应用。

重载函数模板

重载函数模板的工作方式与 C++ 中的常规函数重载完全一样。然而,我会帮助你回忆 C++ 函数重载的基础。

C++ 编译器的函数重载规则和期望如下:

  • 重载的函数名将是相同的。
  • C++ 编译器将无法区分仅在返回值上不同的重载函数。
  • 重载函数参数的数量、这些参数的数据类型或它们的顺序应该不同。除了其他规则之外,当前要点中描述的这些规则中至少有一条应该得到满足,但是更多的遵守不会有什么坏处。
  • 重载函数必须在同一命名空间或同一类范围内。

如果不满足上述任何一条规则,C++ 编译器就不会将它们视为重载函数。如果在区分重载函数时有任何歧义,C++ 编译器会立即将其报告为编译错误。

是时候用一个例子来探讨这个问题了,如下面的程序所示:

#include <iostream>
#include <array>
using namespace std;

void sort ( array<int,6> data ) {

     cout << "Non-template sort function invoked ..." << endl;

     int size = data.size();

     for ( int i=0; i<size; ++ i ) { 
          for ( int j=0; j<size; ++ j ) {
                if ( data[i] < data[j] )
                    swap ( data[i], data[j] );
          }
     }

}

template <typename T, int size>
void sort ( array<T, size> data ) {

     cout << "Template sort function invoked with one argument..." << endl;

     for ( int i=0; i<size; ++ i ) {
         for ( int j=0; j<size; ++ j ) {
             if ( data[i] < data[j] )
                swap ( data[i], data[j] );
         }
     }

}

template <typename T>
void sort ( T data[], int size ) {
     cout << "Template sort function invoked with two arguments..." << endl;

     for ( int i=0; i<size; ++ i ) {
         for ( int j=0; j<size; ++ j ) {
             if ( data[i] < data[j] )
                swap ( data[i], data[j] );
         }
     }

}

int main() {

    //Will invoke the non-template sort function
    array<int, 6> a = { 10, 50, 40, 30, 60, 20 };
    ::sort ( a );

    //Will invoke the template function that takes a single argument
    array<float,6> b = { 10.6f, 57.9f, 80.7f, 35.1f, 69.3f, 20.0f };
    ::sort<float,6>( b );

    //Will invoke the template function that takes a single argument
    array<double,6> c = { 10.6d, 57.9d, 80.7d, 35.1d, 69.3d, 20.0d };
    ::sort<double,6> ( c );

    //Will invoke the template function that takes two arguments
    double d[] = { 10.5d, 12.1d, 5.56d, 1.31d, 81.5d, 12.86d };
    ::sort<double> ( d, 6 );

    return 0;

}

运行以下命令:

g++ main.cpp -std=c++ 17

./a.out

前面程序的输出如下:

Non-template sort function invoked ...

Template sort function invoked with one argument...

Template sort function invoked with one argument...

Template sort function invoked with two arguments...

代码走查

以下代码是我们自定义sort()函数的非模板版本:

void sort ( array<int,6> data ) { 

     cout << "Non-template sort function invoked ..." << endl;

     int size = data.size();

     for ( int i=0; i<size; ++ i ) { 
         for ( int j=0; j<size; ++ j ) {
             if ( data[i] < data[j] )
                 swap ( data[i], data[j] );
         }
     }

}

非模板函数和模板函数可以共存,参与函数重载。前面函数的一个奇怪行为是数组的大小是硬编码的。

我们的sort()函数的第二个版本是一个模板函数,如下面的代码片段所示。有趣的是,我们在第一个非模板sort()版本中注意到的奇怪问题在这里得到了解决:

template <typename T, int size>
void sort ( array<T, size> data ) {

     cout << "Template sort function invoked with one argument..." << endl;

     for ( int i=0; i<size; ++ i ) {
         for ( int j=0; j<size; ++ j ) {
             if ( data[i] < data[j] )
                swap ( data[i], data[j] );
         }
     }

}

在前面的代码中,数据类型和数组大小都作为模板参数传递,然后传递给函数调用参数。这种方法使函数通用,因为该函数可以为任何数据类型实例化。

我们自定义的第三个版本sort()函数也是一个模板函数,如下面的代码片段所示:

template <typename T>
void sort ( T data[], int size ) {

     cout << "Template sort function invoked with two argument..." << endl;

     for ( int i=0; i<size; ++ i ) {
         for ( int j=0; j<size; ++ j ) {
             if ( data[i] < data[j] )
                swap ( data[i], data[j] );
         }
     }

}

前面的模板函数采用 C 风格的数组;因此,它也期望用户指出它的大小。然而,数组的大小可以在函数中计算,但是为了演示的目的,我需要一个接受两个参数的函数。不推荐使用前面的函数,因为它使用了 C 风格的数组;理想情况下,我们会使用其中一个 STL 容器。

现在,让我们了解一下主要的函数代码。下面的代码用六个值声明并初始化 STL 数组容器,然后将这些值传递给我们在默认命名空间中定义的sort()函数:

 //Will invoke the non-template sort function
 array<int, 6> a = { 10, 50, 40, 30, 60, 20 };
 ::sort ( a );

前面的代码将调用非模板sort()函数。需要注意的一点是,每当 C++ 遇到函数调用时,它首先会寻找非模板版本;如果 C++ 找到匹配的非模板函数版本,它对正确函数定义的搜索就到此为止。如果 C++ 编译器无法识别与函数调用签名相匹配的非模板函数定义,那么它将开始寻找能够支持函数调用的任何模板函数,并为所需的数据类型实例化一个专用函数。

让我们理解以下代码:

//Will invoke the template function that takes a single argument
array<float,6> b = { 10.6f, 57.9f, 80.7f, 35.1f, 69.3f, 20.0f };
::sort<float,6>( b );

这将调用接收单个参数的模板函数。由于没有接收array<float,6>数据类型的非模板sort()函数,C++ 编译器将从我们的用户定义的sort()模板函数中实例化这样的函数,单个参数取array<float, 6>

同样,以下代码触发编译器实例化接收array<double, 6>的模板sort()函数的double版本:

  //Will invoke the template function that takes a single argument
 array<double,6> c = { 10.6d, 57.9d, 80.7d, 35.1d, 69.3d, 20.0d };
 ::sort<double,6> ( c );

最后,下面的代码将实例化接收两个参数并调用函数的模板sort()的一个实例:

 //Will invoke the template function that takes two arguments
 double d[] = { 10.5d, 12.1d, 5.56d, 1.31d, 81.5d, 12.86d };
 ::sort<double> ( d, 6 );

如果您已经走了这么远,我相信您会喜欢到目前为止讨论的 C++ 模板主题。

类别模板

C++ 模板也将函数模板的概念扩展到类,并使我们能够编写面向对象的通用代码。在前几节中,您学习了函数模板和重载的使用。在这一节中,您将学习编写模板类,这些类将打开更有趣的泛型编程概念。

class模板允许您通过模板类型表达式在类级别参数化数据类型。

让我们用下面的例子来理解一个class模板:

//myalgorithm.h
#include <iostream>
#include <algorithm>
#include <array>
#include <iterator>
using namespace std;

template <typename T, int size>
class MyAlgorithm {

public:
        MyAlgorithm() { } 
        ~MyAlgorithm() { }

        void sort( array<T, size> &data ) {
             for ( int i=0; i<size; ++ i ) {
                 for ( int j=0; j<size; ++ j ) {
                     if ( data[i] < data[j] )
                         swap ( data[i], data[j] );
                 }
             }
        }

        void sort ( T data[size] );

};

template <typename T, int size>
inline void MyAlgorithm<T, size>::sort ( T data[size] ) {
       for ( int i=0; i<size; ++ i ) {
           for ( int j=0; j<size; ++ j ) {
               if ( data[i] < data[j] )
                  swap ( data[i], data[j] );
           }
       }
}

C++ template function overloading is a form of static or compile-time polymorphism.

让我们在下面的main.cpp程序中使用myalgorithm.h如下:

#include "myalgorithm.h"

int main() {

    MyAlgorithm<int, 10> algorithm1;

    array<int, 10> a = { 10, 5, 15, 20, 25, 18, 1, 100, 90, 18 };

    cout << "\nArray values before sorting ..." << endl;
    copy ( a.begin(), a.end(), ostream_iterator<int>(cout, "\t") );
    cout << endl;

    algorithm1.sort ( a );

    cout << "\nArray values after sorting ..." << endl;
    copy ( a.begin(), a.end(), ostream_iterator<int>(cout, "\t") );
    cout << endl;

    MyAlgorithm<int, 10> algorithm2;
    double d[] = { 100.0, 20.5, 200.5, 300.8, 186.78, 1.1 };

    cout << "\nArray values before sorting ..." << endl;
    copy ( d.begin(), d.end(), ostream_iterator<double>(cout, "\t") );
    cout << endl;

    algorithm2.sort ( d );

    cout << "\nArray values after sorting ..." << endl;
    copy ( d.begin(), d.end(), ostream_iterator<double>(cout, "\t") );
    cout << endl;

    return 0;  

}

让我们使用以下命令快速编译程序:

g++ main.cpp -std=c++ 17

./a.out

输出如下:

Array values before sorting ...
10  5   15   20   25   18   1   100   90   18

Array values after sorting ...
1   5   10   15   18   18   20   25   90   100

Array values before sorting ...
100   20.5   200.5   300.8   186.78   1.1

Array values after sorting ...
1.1     20.5   100   186.78  200.5  300.8

代码走查

下面的代码声明了一个类模板。关键字template <typename T, int size>可以用<class T, int size>代替。两个关键字在函数和类模板中都可以互换;但是,作为行业最佳实践,template<class T>只能与类模板一起使用,以避免混淆:

template <typename T, int size>
class MyAlgorithm 

重载的sort()方法之一定义如下:

 void sort( array<T, size> &data ) {
      for ( int i=0; i<size; ++ i ) {
          for ( int j=0; j<size; ++ j ) {
              if ( data[i] < data[j] )
                 swap ( data[i], data[j] );
          }
      }
 } 

第二个重载sort()函数只是在类范围内声明,没有任何定义,如下:

template <typename T, int size>
class MyAlgorithm {
      public:
           void sort ( T data[size] );
};

前面的sort()函数是在类范围之外定义的,如下面的代码片段所示。奇怪的是,我们需要为类模板之外定义的每个成员函数重复模板参数:

template <typename T, int size>
inline void MyAlgorithm<T, size>::sort ( T data[size] ) {
       for ( int i=0; i<size; ++ i ) {
           for ( int j=0; j<size; ++ j ) {
               if ( data[i] < data[j] )
                  swap ( data[i], data[j] );
           }
       }
}

否则,类模板的概念与函数模板的概念相同。

Would you like to see the compiler-instantiated code for templates? Use the g++ -fdump-tree-original main.cpp -std=c++ 17 command.

显式类专门化

到目前为止,在本章中,您已经学习了如何使用函数模板和类模板进行泛型编程。当您理解类模板时,单个模板类可以支持任何内置和用户定义的数据类型。然而,有时我们需要用相对于其他数据类型的一些特殊处理来处理某些数据类型。在这种情况下,C++ 为我们提供了显式的类专门化支持,以区别对待的方式处理选择性数据类型。

考虑 STL deque容器;虽然deque看起来很适合存储,比如说stringintdoublelong,但是如果我们决定使用deque来存储一堆boolean类型,bool数据类型至少需要一个字节,尽管它可能会因编译器供应商的实现而异。虽然单个位可以有效地表示真或假,但布尔至少需要一个字节,即 8 位,其余 7 位不使用。这可能看起来好像没关系;然而,如果你必须储存大量的布尔值,这显然不是一个有效的想法,对吗?你可能会想,有什么大不了的?我们可以为bool编写另一个专业类或模板类。但是这种方法需要最终用户为不同的数据类型显式地使用不同的类,这听起来也不是一个好的设计,对吗?这正是 C++ 的显式类专门化派上用场的地方。

The explicit template specialization is also referred to as full-template specialization.

如果你还没有被说服,不要紧;下面的示例将帮助您理解显式类专门化的必要性以及显式类专门化是如何工作的。

让我们开发一个DynamicArray类来支持任何数据类型的动态数组。让我们从一个类模板开始,如下面的程序所示:

#include <iostream>
#include <deque>
#include <algorithm>
#include <iterator>
using namespace std;

template < class T >
class DynamicArray {
      private:
           deque< T > dynamicArray;
           typename deque< T >::iterator pos;

      public:
           DynamicArray() { initialize(); }
           ~DynamicArray() { }

           void initialize() {
                 pos = dynamicArray.begin();
           }

           void appendValue( T element ) {
                 dynamicArray.push_back ( element );
           }

           bool hasNextValue() { 
                 return ( pos != dynamicArray.end() );
           }

           T getValue() {
                 return *pos++ ;
           }

};

前面的DynamicArray模板类在内部使用了 STL deque类。因此,您可以将DynamicArray模板类视为自定义适配器容器。让我们通过下面的代码片段来探索如何在main.cpp中使用DynamicArray模板类:

#include "dynamicarray.h"
#include "dynamicarrayforbool.h"

int main () {

    DynamicArray<int> intArray;

    intArray.appendValue( 100 );
    intArray.appendValue( 200 );
    intArray.appendValue( 300 );
    intArray.appendValue( 400 );

    intArray.initialize();

    cout << "\nInt DynamicArray values are ..." << endl;
    while ( intArray.hasNextValue() )
          cout << intArray.getValue() << "\t";
    cout << endl;

    DynamicArray<char> charArray;
    charArray.appendValue( 'H' );
    charArray.appendValue( 'e' );
    charArray.appendValue( 'l' );
    charArray.appendValue( 'l' );
    charArray.appendValue( 'o' );

    charArray.initialize();

    cout << "\nChar DynamicArray values are ..." << endl;
    while ( charArray.hasNextValue() )
          cout << charArray.getValue() << "\t";
    cout << endl;

    DynamicArray<bool> boolArray;

    boolArray.appendValue ( true );
    boolArray.appendValue ( false );
    boolArray.appendValue ( true );
    boolArray.appendValue ( false );

    boolArray.initialize();

    cout << "\nBool DynamicArray values are ..." << endl;
    while ( boolArray.hasNextValue() )
         cout << boolArray.getValue() << "\t";
    cout << endl;

    return 0;

}

让我们使用以下命令快速编译程序:

g++ main.cpp -std=c++ 17

./a.out

输出如下:

Int DynamicArray values are ...
100   200   300   400

Char DynamicArray values are ...
H   e   l   l   o

Bool DynamicArray values are ...
1   0   1   0

太好了。我们的定制适配器容器似乎工作正常。

代码走查

让我们放大,试着理解前面的程序是如何工作的。下面的代码告诉 C++ 编译器,接下来是一个类模板:

template < class T >
class DynamicArray {
      private:
           deque< T > dynamicArray;
           typename deque< T >::iterator pos;

如您所见,DynamicArray类在内部使用了 STL deque,并且为deque声明了一个名为pos的迭代器。这个迭代器posDynamic模板类用来提供高级方法,如initialize()appendValue()hasNextValue()getValue()方法。

initialize()方法将deque迭代器pos初始化为存储在deque中的第一个数据元素。appendValue( T element )方法允许您在deque的末尾添加一个数据元素。hasNextValue()方法告知DynamicArray类是否存储了更多的数据值- true表示它有更多的值,false表示DynamicArray导航已经到达deque的末尾。需要时可以使用initialize()方法将pos迭代器复位到起点。getValue()方法返回此时pos迭代器指向的数据元素。getValue()方法不进行任何验证;因此,在调用getValue()之前,它必须与hasNextValue()相结合,以安全地访问存储在DynamicArray中的值。

现在,我们来了解一下main()功能。下面的代码声明了一个存储int数据类型的DynamicArray类;DynamicArray<int> intArray将触发 C++ 编译器实例化一个专门用于int数据类型的DynamicArray类:

DynamicArray<int> intArray;

intArray.appendValue( 100 );
intArray.appendValue( 200 );
intArray.appendValue( 300 );
intArray.appendValue( 400 );

100200300400DynamicArray类中背靠背存储。下面的代码确保intArray迭代器指向第一个元素。迭代器初始化后,存储在DynamicArray类中的值用getValue()方法打印,而hasNextValue()确保导航没有到达DynamicArray类的末尾:

intArray.initialize();
cout << "\nInt DynamicArray values are ..." << endl;
while ( intArray.hasNextValue() )
      cout << intArray.getValue() << "\t";
cout << endl;

同样,在主函数中,创建一个char DynamicArray类,填充一些数据,并打印出来。让我们跳过DynamicArray直接进入储存boolDynamicArray类。

DynamicArray<bool> boolArray;

boolArray.appendValue ( "1010" );

boolArray.initialize();

cout << "\nBool DynamicArray values are ..." << endl;

while ( boolArray.hasNextValue() )
      cout << boolArray.getValue() << "\t";
cout << endl;

从前面的代码片段中,我们可以看到一切看起来都很好,对吗?是的,前面的代码工作得非常好;然而,DynamicArray设计方法存在性能问题。而true可以用1表示,false可以用0表示,只需要 1 位,前面的DynamicArray类用 8 位表示1,用 8 位表示0,我们必须修正这个问题,不要强迫终端用户选择一个对bool有效的不同的DynamicArray类。

让我们通过使用显式类模板专门化来解决这个问题,代码如下:

#include <iostream>
#include <bitset>
#include <algorithm>
#include <iterator>
using namespace std;

template <>
class DynamicArray<bool> {
      private:
          deque< bitset<8> *> dynamicArray;
          bitset<8> oneByte;
          typename deque<bitset<8> * >::iterator pos;
          int bitSetIndex;

          int getDequeIndex () {
              return (bitSetIndex) ? (bitSetIndex/8) : 0;
          }
      public:
          DynamicArray() {
              bitSetIndex = 0;
              initialize();
          }

         ~DynamicArray() { }

         void initialize() {
              pos = dynamicArray.begin();
              bitSetIndex = 0;
         }

         void appendValue( bool value) {
              int dequeIndex = getDequeIndex();
              bitset<8> *pBit = NULL;

              if ( ( dynamicArray.size() == 0 ) || ( dequeIndex >= ( dynamicArray.size()) ) ) {
                   pBit = new bitset<8>();
                   pBit->reset();
                   dynamicArray.push_back ( pBit );
              }

              if ( !dynamicArray.empty() )
                   pBit = dynamicArray.at( dequeIndex );

              pBit->set( bitSetIndex % 8, value );
              ++ bitSetIndex;
         }

         bool hasNextValue() {
              return (bitSetIndex < (( dynamicArray.size() * 8 ) ));
         }

         bool getValue() {
              int dequeIndex = getDequeIndex();

              bitset<8> *pBit = dynamicArray.at(dequeIndex);
              int index = bitSetIndex % 8;
              ++ bitSetIndex;

              return (*pBit)[index] ? true : false;
         }
};

你注意到模板类声明了吗?模板类专门化的语法是template <> class DynamicArray<bool> { };class模板表达式为空<>,适用于所有数据类型的class模板的名称和适用于bool数据类型的类的名称与模板表达式<bool>保持一致。

如果仔细观察的话,bool的专用DynamicArray类内部使用了deque< bitset<8> >,也就是 8 位的bitsetsdeque,需要时deque会自动分配更多的bitset<8>位。bitset变量是一个内存高效的 STL 容器,只消耗 1 位来表示truefalse

我们来看看main功能:

#include "dynamicarray.h"
#include "dynamicarrayforbool.h"

int main () {

    DynamicArray<int> intArray;

    intArray.appendValue( 100 );
    intArray.appendValue( 200 );
    intArray.appendValue( 300 );
    intArray.appendValue( 400 );

    intArray.initialize();

    cout << "\nInt DynamicArray values are ..." << endl;

    while ( intArray.hasNextValue() )
          cout << intArray.getValue() << "\t";
    cout << endl;

    DynamicArray<char> charArray;

    charArray.appendValue( 'H' );
    charArray.appendValue( 'e' );
    charArray.appendValue( 'l' );
    charArray.appendValue( 'l' );
    charArray.appendValue( 'o' );

    charArray.initialize();

    cout << "\nChar DynamicArray values are ..." << endl;
    while ( charArray.hasNextValue() )
          cout << charArray.getValue() << "\t";
    cout << endl;

    DynamicArray<bool> boolArray;

    boolArray.appendValue ( true );
    boolArray.appendValue ( false );
    boolArray.appendValue ( true );
    boolArray.appendValue ( false );

    boolArray.appendValue ( true );
    boolArray.appendValue ( false );
    boolArray.appendValue ( true );
    boolArray.appendValue ( false );

    boolArray.appendValue ( true );
    boolArray.appendValue ( true);
    boolArray.appendValue ( false);
    boolArray.appendValue ( false );

    boolArray.appendValue ( true );
    boolArray.appendValue ( true);
    boolArray.appendValue ( false);
    boolArray.appendValue ( false );

    boolArray.initialize();

    cout << "\nBool DynamicArray values are ..." << endl;
    while ( boolArray.hasNextValue() )
          cout << boolArray.getValue() ;
    cout << endl;

    return 0;

}

有了类模板专门化,我们可以从下面观察到主代码对于boolchardouble似乎是相同的,尽管主模板类、DynamicArray和专门化的DynamicArray<bool>类是不同的:

DynamicArray<char> charArray;
charArray.appendValue( 'H' );
charArray.appendValue( 'e' );

charArray.initialize();

cout << "\nChar DynamicArray values are ..." << endl;
while ( charArray.hasNextValue() )
cout << charArray.getValue() << "\t";
cout << endl;

DynamicArray<bool> boolArray;
boolArray.appendValue ( true );
boolArray.appendValue ( false );

boolArray.initialize();

cout << "\nBool DynamicArray values are ..." << endl;
while ( boolArray.hasNextValue() )
      cout << boolArray.getValue() ;
cout << endl;

我相信你会发现这个 C++ 模板专门化特性非常有用。

部分模板专门化

与显式模板专门化不同,显式模板专门化用它自己对特定数据类型的完整定义来替换主模板类,部分模板专门化允许我们专门化主模板类支持的模板参数的特定子集,而其他泛型类型可以与主模板类相同。

当部分模板专门化与继承相结合时,它可以创造更多奇迹,如下例所示:

#include <iostream>
using namespace std;

template <typename T1, typename T2, typename T3>
class MyTemplateClass {
public:
     void F1( T1 t1, T2 t2, T3 t3 ) {
          cout << "\nPrimary Template Class - Function F1 invoked ..." << endl;
          cout << "Value of t1 is " << t1 << endl;
          cout << "Value of t2 is " << t2 << endl;
          cout << "Value of t3 is " << t3 << endl;
     }

     void F2(T1 t1, T2 t2) {
          cout << "\nPrimary Tempalte Class - Function F2 invoked ..." << endl;
          cout << "Value of t1 is " << t1 << endl;
          cout << "Value of t2 is " << 2 * t2 << endl;
     }
};
template <typename T1, typename T2, typename T3>
class MyTemplateClass< T1, T2*, T3*> : public MyTemplateClass<T1, T2, T3> {
      public:
          void F1( T1 t1, T2* t2, T3* t3 ) {
               cout << "\nPartially Specialized Template Class - Function F1 invoked ..." << endl;
               cout << "Value of t1 is " << t1 << endl;
               cout << "Value of t2 is " << *t2 << endl;
               cout << "Value of t3 is " << *t3 << endl;
          }
};

main.cpp文件将包含以下内容:

#include "partiallyspecialized.h"

int main () {
    int x = 10;
    int *y = &x;
    int *z = &x;

    MyTemplateClass<int, int*, int*> obj;
    obj.F1(x, y, z);
    obj.F2(x, x);

    return 0;
}

从前面的代码中,您可能已经注意到主模板类名和部分专门化类名与完全或显式模板类专门化的情况相同。但是,模板参数表达式中有一些语法变化。在完全模板类专门化的情况下,模板参数表达式将为空,而在部分专门化的模板类的情况下,将出现列出的,如下所示:

template <typename T1, typename T2, typename T3>
class MyTemplateClass< T1, T2*, T3*> : public MyTemplateClass<T1, T2, T3> { };

表达式template<typename T1, typename T2, typename T3>是主类模板类中使用的模板参数表达式,MyTemplateClass< T1, T2*, T3*>是第二类完成的部分特化。如你所见,第二类对typename T2typename T3做了一些特殊化,因为它们在第二类中被用作指针;然而,typename T1被用作是在第二类。

除了到目前为止讨论的事实之外,第二个类还继承了主模板类,这有助于第二个类重用主模板类的公共和受保护的方法。然而,部分模板专门化并不能阻止专门化类支持其他功能。

当主模板类的F1函数被部分专门化的模板类替换时,它通过继承重用主模板类的F2函数。

让我们使用以下命令快速编译程序:

g++ main.cpp -std=c++ 17

./a.out

程序的输出如下:

Partially Specialized Template Classs - Function F1 invoked ...
Value of t1 is 10
Value of t2 is 10
Value of t3 is 10

Primary Tempalte Class - Function F2 invoked ...
Value of t1 is 10
Value of t2 is 20

我希望你发现部分专门化的模板类是有用的。

摘要

在本章中,您学习了以下内容:

  • 您现在知道了使用泛型编程的动机
  • 您现在已经熟悉了函数模板
  • 你知道如何霸王功能模板
  • 您知道类模板
  • 您知道什么时候使用显式模板专门化,什么时候使用部分专门化的模板专门化

恭喜你!总的来说,你对 C++ 的模板编程有很好的理解。

在下一章中,您将学习智能指针。