2008-11-27 67 views
1

假設大約100個文件包含大約100個包含大約200,000行代碼的模板的大型模板庫。某些模板使用多重繼承來使庫本身的使用變得相當簡單(即從某些基本模板繼承而來,只需執行某些業務規則)。如何最好地從模板混亂切換到清潔類架構(C++)?

所有存在(生長多年),「工程」,並用於項目。

但是,使用該庫的項目的編譯會消耗更多的時間,並且需要相當長的一段時間才能找到某些錯誤的源代碼。修復通常會導致意想不到的副作用,或者相當困難,因爲一些相互依賴的模板需要更改。由於功能很多,測試幾乎是不可能的。

現在,我真的想簡化架構使用更少的模板和更專業的小類。

是否有任何經過驗證的方式去完成這項任務?什麼是開始的好地方?

回答

13

我不確定我是否知道模板是如何/爲什麼是問題,以及爲什麼簡單的非模板類會有所改進。難道這不僅僅意味着即使更多類,更少的類型安全和更大的潛在錯誤?

我可以理解,簡化體系結構,重構和刪除各種類和模板之間的依賴關係,但自動假設「更少的模板將使架構更好」是有缺陷的。

我想說,模板可能允許你建立一個比你沒有它們更清潔的架構。僅僅因爲你可以獨立完成完全。如果沒有模板,調用其他類的類函數必須事先知道該類或其繼承的接口。通過模板,這種耦合不是必需的。

刪除模板只會導致更多依賴關係,不會更少。 添加的模板類型安全性可用於在編譯時檢測大量的錯誤(爲此目的將static_assert放在代碼中)

當然,增加的編譯時間可能是一個有效的理由在某些情況下避免使用模板,如果您只有一大堆習慣於使用「傳統」OOP術語思考的Java程序員,模板可能會混淆它們,這可能是避免使用模板的另一個有效原因。

但是從架構的角度來看,我認爲避免模板是朝着錯誤方向邁出的一步。

重構應用程序,當然,這聽起來像是需要的。但是,不要因爲原始版本的應用程序濫用它而丟棄用於生成可擴展和可靠代碼的最有用的工具之一。特別是如果您已經對代碼量感到擔憂,那麼刪除模板很可能會導致更多代碼行。

+0

好點!感謝讓我以不同的方向思考。 – 2008-11-27 17:22:40

2

那麼,問題在於模板的思維方式與面向對象的基於繼承的方式有很大的不同。很難回答「重新設計整個事情並從頭開始」。

當然,對於特定情況可能有一個簡單的方法。我們不知道你有什麼更多的知識。

無論如何,模板解決方案如此難以維護的事實反映了糟糕的設計。

+0

我同意這聽起來像真正的問題不是模板,而是糟糕的設計。 – 2008-11-27 16:52:47

7

你需要自動化測試,這種方式在十年後,當你的succesor有同樣的問題時,他可以重構代碼(可能添加更多的模板,因爲他認爲這將簡化庫的使用),並知道它仍然符合所有測試用例。同樣,任何小錯誤修復的副作用都將立即可見(假設您的測試用例很好)。

除此之外, 「分而conqueor」

3

編寫單元測試。

新代碼必須與舊代碼相同。

這是至少一個提示。

編輯:

如果您棄用您已經用新的功能,你可以 一點逐步轉移到新的代碼替換小舊代碼。

0

如前所述,單元測試是一個好主意。事實上,不是通過引入可能會波及的「簡單」更改來破壞代碼,而是專注於創建一套測試以及修復不符合測試的問題。在發現錯誤時開展更新測試的活動。

除此之外,我會建議升級您的工具,如果可能的話,以幫助調試模板相關的問題。

2

有些觀點(但是注意:這些都是不是確實是邪惡的。如果你想改變非模板代碼,雖然,這可以幫助出):


查找您的靜態接口。模板依賴於哪些功能存在?他們在哪裏需要typedefs?

將公共部分放在抽象基類中。一個很好的例子就是當你偶然發現CRTP的成語時。你可以用具有虛函數的抽象基類來替換它。

查找整數列表。如果您發現代碼使用整數列表(如list<1, 3, 3, 1, 3>),則可以用std::vector替換它們,如果所有使用它們的代碼都可以使用運行時值而不是常量表達式。

查找類型特徵。有很多代碼涉及檢查是否存在某種typedef,或者在典型的模板代碼中是否存在某種方法。抽象基類通過使用純虛方法和通過繼承typedef來解決這兩個問題。通常情況下,typedefs只需觸發可怕的功能,如SFINAE,這將是多餘的。

查找表達式模板。如果您的代碼使用表達式模板來避免創建臨時對象,則必須消除它們,並使用傳統方式將臨時對象返回/傳遞給涉及的操作符。

查找功能對象。如果你發現你的代碼使用了函數對象,你可以改變它們來使用抽象基類,並且有類似void run();的東西來調用它們(或者如果你想繼續使用operator(),那麼更好!它也可以是虛擬的)。

0

我經常遇到很大的遺留模板,需要很多時間和內存來實例化,但並不需要。在這些情況下,最簡單的方法是去掉所有不依賴任何模板參數的代碼,並將其隱藏在正常翻譯單元中定義的單獨函數中。當這個代碼必須稍作修改或文檔發生變化時,這也具有觸發更少重編譯的積極副作用。這聽起來相當明顯,但真正讓人驚訝的是,人們編寫一個類模板的頻率並認爲它所做的每件事都必須在標題中定義,而不僅僅是需要模板化信息的代碼。

您可能要考慮的另一件事是,您經常通過將模板「mixin」樣式而不是多重繼承的聚合來清理繼承層次結構。通過使模板參數之一成爲它應從中派生出的基類的名稱(請參見boost::enable_shared_from_this的工作方式),查看有多少地方可以獲得。當然,如果構造函數不帶任何參數,這通常只會工作得很好,因爲您不必擔心正確地初始化任何東西。

1

據我所知,你最關心的是編譯時間和庫的可維護性?

首先,不要試圖一次性「修復」。

其次,明白你​​修好了什麼。模板的複雜性經常出於某種原因,例如執行某些用途,並使編譯器幫助你不犯錯誤。這個原因有時可能會被採用到很遠,但是拋出100條線是因爲「沒有人真的知道他們做了什麼」不應該被忽略。我在這裏建議的一切都可以引入真正令人討厭的錯誤,你已經受到警告。

第三,首先考慮更便宜的修復:例如,更快的機器或分佈式構建工具。至少要扔掉所有的板子,然後扔掉舊的磁盤。它確實會產生差異。一個驅動器的操作系統,一個驅動器的構建是一個便宜的男人RAID。

圖書館是否有詳細記錄?這是您最好的機會,可以查看諸如doxygen等工具來幫助您創建這樣的文檔。

全部考慮?好了,現在一些建議,爲構建時間;)


瞭解C++ build model:每次的.cpp單獨編譯。這意味着許多頭文件很多.cpp文件=巨大的構建。這不是建議將所有內容放入一個.cpp文件,但!但是,一個可以加速編譯速度的技巧(!)是創建一個包含一堆.cpp文件的.cpp文件,並且只將該「主」文件提供給編譯器。但是,你不能盲目做這件事 - 你需要了解可能引入的錯誤類型。

如果你還沒有一個,得到一個單獨的生成機,你可以遠程進入。你將不得不做很多幾乎完整的構建來檢查你是否打破了一些包含。你會想在另一臺機器上運行它,這並不會阻止你在其他機器上工作。長期來看,無論如何,您都需要它進行日常集成構建;)

使用預編譯頭文件。 (使用快速機器可以更好地進行縮放,參見上文)

檢查標題包含策略。雖然每個文件都應該是「獨立的」(即包括其他人需要包含的所有內容),但不要寬泛地包含。不幸的是,我還沒有找到找到不必要的#incldue語句的工具,但它可能有助於花費一些時間去除「熱點」文件中未使用的標題。

爲您使用的模板創建並使用正向聲明。通常情況下,您可以在許多地方使用forwad聲明來包含頭文件,並且僅在少數特定的頭文件中使用完整頭文件。這可以極大地幫助編譯時間。檢查<iosfwd>標題標準庫如何處理I/O流。

重載少品種模板:如果你有一個複雜的函數模板只爲極少數類型,如這是有用的:

// .h 
template <typename FLOAT> // float or double only 
FLOAT CalcIt(int len, FLOAT * values) { ... } 

可以聲明在標題中過載和動模板到身體:

// .h 
float CalcIt(int len, float * values); 
double CalcIt(int len, double * values); 

// .cpp 
template <typename FLOAT> // float or double only 
FLOAT CalcItT(int len, FLOAT * values) { ... } 

float CalcIt(int len, float * values) { return CalcItT(len, values); } 
double CalcIt(int len, double * values) { return CalcItT(len, values); } 

這將冗長的模板移動到單個編譯單元。
不幸的是,這只是對類的有限使用。

檢查the PIMPL idiom是否可以將代碼從頭文件移動到.cpp文件中。

隱藏在那裏的一般規則是將庫的接口與實現分開。請使用註釋detail命名空間和單獨的.impl.h標題,從頭腦到物理隔離外部應該知道的內容和完成的方式。這暴露了你的庫的真正價值(它實際上是否封裝了複雜性?),並且讓你有機會首先替換「簡單目標」。


更具體的建議 - 以及如何有用的給予是 - 很大程度上取決於實際的圖書館。

祝你好運!