程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> Item 44:將參數無關代碼重構到模板外

Item 44:將參數無關代碼重構到模板外

編輯:關於C++

Item 44: Factor parameter-independent code out of templates.

模板是個好東西,你可以在實現類型安全的同時少寫很多代碼。但模板提供的是編譯期的多態, 即使你的代碼看起來非常簡潔短小,生成的二進制文件也可能包含大量的冗余代碼。 因為模板每次實例化都會生成一個完整的副本,所以其中與模板參數無關的部分會造成代碼膨脹(code bloat)。

把模板中參數無關的代碼重構到模板外便可以有效地控制模板產生的代碼膨脹。 另外代碼膨脹也可以由類型模板參數產生:

  • 對於非類型模板參數產生的代碼膨脹,用函數參數或數據成員來代替模板參數即可消除冗余;
  • 對於類型模板參數產生的代碼膨脹,可以讓不同實例化的模板類共用同樣的二進制表示。

    抽取公共代碼

    在避免代碼冗余的問題上,抽取公共代碼(commonality and variability analysis)是我們每天都在用的方法。 當你寫幾個函數時,會把其中的公共部分抽取到另一個函數;當你聲明類時,也會把它們的公共部分抽取到父類中。

    於是你希望在模板編程中也用該辦法來避免代碼重復,但模板和非模板代碼在這一點上是不同的:

    • 非模板的代碼中,冗余的顯式的(explicit)。只要有重復代碼你都會看到它;
    • 模板代碼中,冗余是隱式的(implicit)。模板代碼只有一份,模板被實例化時產生的冗余需要你的直覺才能感受到。

      模板產生的代碼膨脹

      現在來看一個模板是怎樣引發代碼膨脹的。比如要實現一個固定大小的矩陣,它支持轉置運算。

      template
      class Square{
      public:
          void invert();
      };
      

      其中的int n是一個非類型參數,它也是一種合法的模板參數~

      然後可能會這樣使用該模板:

      Square s1;
      Square s2;
      s1.invert();    s2.invert();
      

      Square模板會實例化兩個類:SquareSquare,它們擁有相同的invert方法。 這是模板產生代碼膨脹的典型場景。

      抽取父類模板

      結局模板產生的代碼膨脹,仍然是用抽取公共代碼的辦法。如果你真的看到了二進制代碼中兩個相同的invert函數, 你的直覺肯定是把它抽取到另一個類中:

      template
      class SquareBase{
      protected:
          void invert(int size);
      };
      
      template
      class Square:private SquareBase{
      private:
          using SquareBase::invert;
      public:
          void invert(){ this->invert(n); }
      }
      

      因為invert函數定義在基類中,所以它只會在二進制代碼中出現一次,即SquareBase::invert。該函數由兩個子類共享。 上述代碼中有些細節還值得一提:

      • SquareBase::invert是供子類用的,所以聲明為private而不是public
      • 調用父類invert的代價為零,因為Square::invert是隱式的inline函數,見Item 30;
      • 使用this->前綴是因為,SquareBase裡的名稱在子類模板Square裡是隱藏的,見Item 43;
      • 使用private繼承是因為,Squareis implemented in terms ofSquare,見Item 39。

        數據存儲問題

        既然我們決定由父類來做invert操作,那麼父類怎麼訪問數據呢?因為數據本來是在子類中的。 當然我們可以在調用SquareBase::invert時把內存地址也一起告知父類, 但如果矩陣類中有很多函數都需要這些信息呢?我們可能需要調用每個函數時都把這些信息傳遞給父類函數。 既然這樣,何不把數據地址直接放在父類中?既然父類存放了數據,那麼就把矩陣大小也一並存放了吧!

        template
        class SquareBase{
        protected:
            SquareBase(int _n, T *_data): n(_n), data(_data){}
            void setData(T *_data){
                data = _data;
            }
        private:
            int n;
            T* data;
        };
        

        父類中存儲了矩陣數據的位置(data)以及大小(n),子類仍然可以決定如何分配地址空間。 可以存放在子類中作為成員屬性,也可以動態申請內存。

        權衡

        不管數據是怎樣分配和訪問的,我們消除代碼重復的方案是確定的:將公共部分抽取到父模板類中。 這樣做的好處便是避免了代碼膨脹,減小了二進制文件和”working set”的大小,有利於提高指令緩存的命中率, 從而達到更高的代碼執行效率。但提取公共部分到新的模板類也造成了一些問題:

        • 如果int n硬編碼在模板參數中的話,編譯器能做更多的優化,比如常量傳播等。但int n作為函數參數,這些優化就沒有了。
        • 添加類的層級會導致對象大小的增加。至少多存儲了一個T* data指針。

          實踐中到底是否應該抽取公共代碼出來取決於你的應用場景,在上述的優劣中進行權衡。

          本問討論的是非類型模板參數,對於類型模板參數,代碼膨脹的問題也是存在的,比如

          • intlong在多數平台都是一樣的底層實現,然而模板卻會實例化為兩份,因為它們類型不同。
          • List,List,List的底層實現也是一樣的。但因為指針類型不同,也會實例化為多份模板類。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved