C++ 自定義比較:仿函數、函數與重載操作符

cpp 模板泛型編程

cpp 比 c 方便不少不光因爲其支持面向對象支持class,同樣還因爲其支持泛型編程,有方便的STL庫。泛型要比宏強大的多,是一種設計更巧妙的編譯期動態機制,類型安全,使得一些通用算法的封裝變得十分方便。模板操作的是類型,特化的時候編譯器會做類型推導,這是模板一個核心特徵。
根據C++標準,當一個模板不被用到時它就不應該被具體化。對於cpp 編譯器是如何特化,編譯成最終代碼,用到了 惰性求值模式匹配。這篇文章簡單介紹了這兩個原理:學習Haskell
對於實際的使用,記住下面兩點:

  • 函數模板的模板參數是隱式的,編譯器根據傳入值的類型來推導模板參數的類型,函數模板的參數不能有默認值。
  • 類模板的模板參數是顯式的,使用一個類模板時必須指明其使用的參數,類模板的模板參數可以有默認值。

這裏主要說一下 C++ 標準庫,尤其是 STL 涉及比較操作時的常用比較器。這裏是STL源代碼

對 sort, stable_sort 傳定製比較函數

傳入函數編譯器來做類型推導,進行模板特化。看sort函數的模板聲明:

// 可以看出,排序要求容器支持隨機訪問迭代器,類似於數組的那種下標偏移訪問
// 這裏 _Compare 是類型, __comp 是實例,調用 sort 需要傳入的就是 __comp 實例
template <class _RandomAccessIter, class _Compare>
inline void sort(_RandomAccessIter __first, _RandomAccessIter __last,
                 _Compare __comp)

    一個例子如下:

    typedef struct tagNode {
        int index;
        int value;
    } Node;
    
    bool comp(const Node &a, const Node &b)
    {
        return a.value > b.value;
    }
    
    int main()
    {
        Node a[5] = { {1, 11}, {3, 33}, {2, 22}, {5, 55}, {4, 44} };
        // 編譯器會進行類型推導做模板特化 <class _RandomAccessIter, class _Compare>
        sort(a, a + 5, comp);
        return 0;
    }

      對 map, set, priority_queue 傳入仿函數

      仿函數functor的英文解釋爲something that performs a function,即其行爲類似函數的東西。C++中的仿函數是通過在類中重載()運算符實現,使你可以像使用函數一樣來創建類的對象。要求傳入仿函數的地方也很好理解,一般C++模板,尖括號<>裏面放的是類型,自然這需要比較器的時候傳入的也是比較器的類型,這就是用到仿函數的地方。
      首先,看一下 STL 源碼的模板聲明:

      // map 和 set 底層存儲結構式都是紅黑樹
      // Forward declarations of operators == and <, needed for friend declarations.
      template <class _Key, class _Tp,
                class _Compare __STL_DEPENDENT_DEFAULT_TMPL(less<_Key>),
                class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
      class map;
      
      template <class _Key, class _Compare __STL_DEPENDENT_DEFAULT_TMPL(less<_Key>),
                class _Alloc = __STL_DEFAULT_ALLOCATOR(_Key) >
      class set;
      
      // priority_queue 有優先級,要求元素可比較。queue 和 priority_queue 默認的底層存儲結構也不同
      // queue 默認用的是 deque 雙端隊列,priority_queue 用的是 vector
      // priority_queue 實現使用的默認比較是 operator< ,是最大堆數據結構,即隊列頭元素值最大
      template <class _Tp,
                class _Sequence __STL_DEPENDENT_DEFAULT_TMPL(deque<_Tp>) >
      class queue;
      
      template <class _Tp,
                class _Sequence __STL_DEPENDENT_DEFAULT_TMPL(vector<_Tp>),
                class _Compare
                __STL_DEPENDENT_DEFAULT_TMPL(less<typename _Sequence::value_type>)
      class priority_queue;  // 注意點:如果自己傳入自己的仿函數比較,那麼第二個存儲類型也要傳入不能缺省

        對於 priority_queue 用法,這篇文章裏的例子很不錯STL裏的priority_queue用法
        一個注意點就是:如果使用自定義比較,使用仿函數,那麼使用時必然也要傳入第二個模板類型參數,要麼都缺省,要麼都傳入。下面給一個例子:

        #include <queue>
        #include <cstdio>
        
        using namespace std;
        
        typedef struct tagNode
        {
            int index;
            int value;
        } Node;
        
        /* 針對某種類型的自定義比較仿函數,cpp 中 struct 相當於全部 public 的 class */
        struct classcomp
        {
            /* const 這裏編譯沒有約束,但是對於明確的不可變參加上更嚴謹 */
            bool operator() (const Node& a, const Node& b) const
            {
                return a.value > b.value;
            }
        };
        
        int main()
        {
            Node a[5] = { {1, 11}, {3, 33}, {2, 22}, {5, 55}, {4, 44} };
        
            // classcomp 必須是針對 Node 類型的比較仿函數,第二個缺省存儲結構也不能少
            priority_queue<Node, vector<Node>, classcomp> priQue;
        
            for(unsigned int i = 0; i < sizeof(a)/sizeof(a[0]); ++i) {
                priQue.push(a[i]);
            }
            while(!priQue.empty()) {
                const Node& topNode = priQue.top();
                // Node topNode = priQue.top() // 也可以結構體直接 = 號賦值,底層相當於 memcpy, Linux 內核就有這麼用的
                // const Node *ptopNode = &priQue.top(); // 如果不想拷貝,cpp 推薦用引用, 即上面的寫法
                printf("index is %d, value is %d\n", topNode.index, topNode.value);
                priQue.pop();
            }
            return 0;
        }

          重載 < 運算符

          重載<運算符,這種方式通用性會比較強,這種方式使得複雜結構變得像基本數據類型一樣可比較了。這樣就不用傳比較器了,因爲默認的比較器這時已經生效了。對於 cpp 代碼,重載運算符不管對於 struct 還是 class 都是很方便的,都是在自己定義的結構體或類類型里加一個重載的函數即可。這種方法缺點也是比較明顯的,如果有兩個模板容器對同一自定義數據結構(結構體或者類)需要不同的比較器,那麼重載 < 這種方法就沒有上面的自定義比較器(比較函數或者仿函數)適用了。
          同樣給個例子:

          #include <queue>
          #include <cstdio>
          
          using namespace std;
          
          // 重載運算符 讓cpp代碼的表達力有很大提升,比如map重載[]可以翻遍用[]獲取指定key的value
          // 還有如果定義了一些矩陣運算什麼的,重載 * 就更加方便靈活了
          
          struct Node
          {
              int index;
              int value;
              /* 注意點:這裏的 const 不可少編譯約束 */
              bool operator < (const Node& compNode) const
              {
                  return this->value < compNode.value;
              }
          };
          
          
          int main()
          {
              Node a[5] = { {1, 11}, {3, 33}, {2, 22}, {5, 55}, {4, 44} };
          
              priority_queue<Node> priQue; //Node 類型重載了 < ,變得像 int 等基本數據類型一樣可比較了
          
              for(unsigned int i = 0; i < sizeof(a)/sizeof(a[0]); ++i) {
                  priQue.push(a[i]);
              }
              while(!priQue.empty()) {
                  const Node& topNode = priQue.top();
                  printf("index is %d, value is %d\n", topNode.index, topNode.value);
                  priQue.pop();
              }
              return 0;
          }

            總結

            對於 cpp 中需要自定義比較時,如果以後的操作比較都是定的,可以用重載,否則還是用自定義比較函數和仿函數。還有就是STL 中優先級隊列priority_queue, 自定義仿函數是模板的第三個類型,如果自定了那麼第二個模板參數也要傳入,一般用vector就可以。

            原文鏈接

            C++ 自定義比較:仿函數、函數與重載操作符

            發表評論
            所有評論
            還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
            相關文章