多態(polymorphism)一詞最初來源於希臘語polumorphos,含義是具備多種形式或形態的情形。在程序設計領域,一個普遍承認的定義是 「一種將不一樣的特殊行爲和單個泛化記號相關聯的能力」。和純粹的面向對象程序設計語言不一樣,C++中的多態有着更普遍的含義。除了常見的經過類繼承和虛函 數機制生效於運行期的動態多態(dynamic polymorphism)外,模板也容許將不一樣的特殊行爲和單個泛化記號相關聯,因爲這種關聯處理於編譯期而非運行期,所以被稱爲靜態多態 (static polymorphism)。
事實上,帶變量的宏和函數重載機制也容許將不一樣的特殊行爲和單個泛化記號相關聯。然而,習慣上咱們並不將它們展示出來的行爲稱爲多態(或靜態多態)。今 天,當咱們談及多態時,若是沒有明確所指,默認就是動態多態,而靜態多態則是指基於模板的多態。不過,在這篇以C++各類多態技術爲主題的文章中,咱們首 先仍是回顧一下C++社羣爭論已久的另外一種「多態」:函數多態(function polymorphism),以及更不常提的「宏多態(macro polymorphism)」。 ios
也就是咱們常說的函數重載(function overloading)。基於不一樣的參數列表,同一個函數名字能夠指向不一樣的函數定義: 算法
// overload_poly.cpp #include <iostream> #include <string> // 定義兩個重載函數 int my_add(int a, int b) { return a + b; } int my_add(int a, std::string b) { return a + atoi(b.c_str()); } int main() { int i = my_add(1, 2); // 兩個整數相加 int s = my_add(1, "2"); // 一個整數和一個字符串相加 std::cout << "i = " << i << "\n"; std::cout << "s = " << s << "\n"; }
根據參數列表的不一樣(類型、個數或兼而有之),my_add(1, 2)和my_add(1, "2")被分別編譯爲對my_add(int, int)和my_add(int, std::string)的調用。實現原理在於編譯器根據不一樣的參數列表對同名函數進行名字重整,然後這些同名函數就變成了彼此不一樣的函數。比方說,也許 某個編譯器會將my_add()函數名字分別重整爲my_add_int_int()和my_add_int_str()。
編程
帶變量的宏能夠實現一種初級形式的靜態多態: 設計模式
// macro_poly.cpp #include <iostream> #include <string> // 定義泛化記號:宏ADD #define ADD(A, B) (A) + (B); int main() { int i1(1), i2(2); std::string s1("Hello, "), s2("world!"); int i = ADD(i1, i2); // 兩個整數相加 std::string s = ADD(s1, s2); // 兩個字符串「相加」 std::cout << "i = " << i << "\n"; std::cout << "s = " << s << "\n"; }
當程序被編譯時,表達式ADD(i1, i2)和ADD(s1, s2)分別被替換爲兩個整數相加和兩個字符串相加的具體表達式。整數相加體現爲求和,而字符串相加則體現爲鏈接。程序的輸出結果符合直覺:
1 + 2 = 3
Hello, + world! = Hello, world!
安全
這就是衆所周知的的多態。現代面嚮對象語言對這個概念的定義是一致的。其技術基礎在於繼承機制和虛函數。例如,咱們能夠定義一個抽象基類Vehicle和兩個派生於Vehicle的具體類Car和Airplane: 網絡
// dynamic_poly.h #include <iostream> // 公共抽象基類Vehicle class Vehicle { public: virtual void run() const = 0; }; // 派生於Vehicle的具體類Car class Car: public Vehicle { public: virtual void run() const { std::cout << "run a car\n"; } }; // 派生於Vehicle的具體類Airplane class Airplane: public Vehicle { public: virtual void run() const { std::cout << "run a airplane\n"; } };
客戶程序能夠經過指向基類Vehicle的指針(或引用)來操縱具體對象。經過指向基類對象的指針(或引用)來調用一個虛函數,會致使對被指向的具體對象之相應成員的調用:框架
// dynamic_poly_1.cpp #include <iostream> #include <vector> #include "dynamic_poly.h" // 經過指針run任何vehicle void run_vehicle(const Vehicle* vehicle) { vehicle->run(); // 根據vehicle的具體類型調用對應的run() } int main() { Car car; Airplane airplane; run_vehicle(&car); // 調用Car::run() run_vehicle(&airplane); // 調用Airplane::run() }
此例中,關鍵的多態接口元素爲虛函數run()。因爲run_vehicle()的參數爲指向基類Vehicle的指針,於是沒法在編譯期決定使用哪個 版本的run()。在運行期,爲了分派函數調用,虛函數被調用的那個對象的完整動態類型將被訪問。這樣一來,對一個Car對象調用run_vehicle (),實際上將調用Car::run(),而對於Airplane對象而言將調用Airplane::run()。
或許動態多態最吸引人之處在於處理異質對象集合的能力:
ide
// dynamic_poly_2.cpp #include <iostream> #include <vector> #include "dynamic_poly.h" // run異質vehicles集合 void run_vehicles(const std::vector<Vehicle*>& vehicles) { for (unsigned int i = 0; i < vehicles.size(); ++i) { vehicles[i]->run(); // 根據具體vehicle的類型調用對應的run() } } int main() { Car car; Airplane airplane; std::vector<Vehicle*> v; // 異質vehicles集合 v.push_back(&car); v.push_back(&airplane); run_vehicles(v); // run不一樣類型的vehicles }
在run_vehicles()中,vehicles[i]->run()依據正被迭代的元素的類型而調用不一樣的成員函數。這從一個側面體現了面向對象編程風格的優雅。
函數
若是說動態多態是經過虛函數來表達共同接口的話,那麼靜態多態則是經過「彼此單獨定義但支持共同操做的具體類」來表達共同性,換句話說,必須存在必需的同名成員函數。
性能
咱們能夠採用靜態多態機制重寫上一節的例子。這一次,咱們再也不定義vehicles類層次結構,相反,咱們編寫彼此無關的具體類Car和Airplane(它們都有一個run()成員函數):
// static_poly.h #include <iostream> //具體類Car class Car { public: void run() const { std::cout << "run a car\n"; } }; //具體類Airplane class Airplane { public: void run() const { std::cout << "run a airplane\n"; } }; run_vehicle()應用程序被改寫以下: // static_poly_1.cpp #include <iostream> #include <vector> #include "static_poly.h" // 經過引用而run任何vehicle template <typename Vehicle> void run_vehicle(const Vehicle& vehicle) { vehicle.run(); // 根據vehicle的具體類型調用對應的run() } int main() { Car car; Airplane airplane; run_vehicle(car); // 調用Car::run() run_vehicle(airplane); // 調用Airplane::run() }
如今Vehicle用做模板參數而非公共基類對象(事實上,這裏的Vehicle只是一個符合直覺的記號而已,此外別無它意)。通過編譯器處理後,咱們最終會獲得run_vehicle<Car>()和 run_vehicle<Airplane>()兩個不一樣的函數。這和動態多態不一樣,動態多態憑藉虛函數分派機制在運行期只有一個run_vehicle()函數。
咱們沒法再透明地處理異質對象集合了,由於全部類型都必須在編譯期予以決定。不過,爲不一樣的vehicles引入不一樣的集合只是舉手之勞。因爲無需再將集合元素侷限於指針或引用,咱們如今能夠從執行性能和類型安全兩方面得到好處:
// static_poly_2.cpp #include <iostream> #include <vector> #include "static_poly.h" // run同質vehicles集合 template <typename Vehicle> void run_vehicles(const std::vector<Vehicle>& vehicles) { for (unsigned int i = 0; i < vehicles.size(); ++i) { vehicles[i].run(); // 根據vehicle的具體類型調用相應的run() } } int main() { Car car1, car2; Airplane airplane1, airplane2; std::vector<Car> vc; // 同質cars集合 vc.push_back(car1); vc.push_back(car2); //vc.push_back(airplane1); // 錯誤:類型不匹配 run_vehicles(vc); // run cars std::vector<Airplane> vs; // 同質airplanes集合 vs.push_back(airplane1); vs.push_back(airplane2); //vs.push_back(car1); // 錯誤:類型不匹配 run_vehicles(vs); // run airplanes }
在一些高級C++應用中,咱們可能須要結合使用動態多態和靜態多態兩種機制,以期達到對象操做的優雅、安全和高效。例如,咱們既但願一致而優雅地處理 vehicles的run問題,又但願「安全而高效」地完成給飛行器(飛機、飛艇等)進行「空中加油」這樣的高難度動做。爲此,咱們首先將上面的 vehicles類層次結構改寫以下:
// dscombine_poly.h #include <iostream> #include <vector> // 公共抽象基類Vehicle class Vehicle { public: virtual void run() const = 0; }; // 派生於Vehicle的具體類Car class Car: public Vehicle { public: virtual void run() const { std::cout << "run a car\n"; } }; // 派生於Vehicle的具體類Airplane class Airplane: public Vehicle { public: virtual void run() const { std::cout << "run a airplane\n"; } void add_oil() const { std::cout << "add oil to airplane\n"; } }; // 派生於Vehicle的具體類Airship class Airship: public Vehicle { public: virtual void run() const { std::cout << "run a airship\n"; } void add_oil() const { std::cout << "add oil to airship\n"; } };
咱們理想中的應用程序能夠編寫以下:
// dscombine_poly.cpp #include <iostream> #include <vector> #include "dscombine_poly.h" // run異質vehicles集合 void run_vehicles(const std::vector<Vehicle*>& vehicles) { for (unsigned int i = 0; i < vehicles.size(); ++i) { vehicles[i]->run(); // 根據具體的vehicle類型調用對應的run() } } // 爲某種特定的aircrafts同質對象集合進行「空中加油」 template <typename Aircraft> void add_oil_to_aircrafts_in_the_sky(const std::vector<Aircraft>& aircrafts) { for (unsigned int i = 0; i < aircrafts.size(); ++i) { aircrafts[i].add_oil(); } } int main() { Car car1, car2; Airplane airplane1, airplane2; Airship airship1, airship2; std::vector<Vehicle*> v; // 異質vehicles集合 v.push_back(&car1); v.push_back(&airplane1); v.push_back(&airship1); run_vehicles(v); // run不一樣種類的vehicles std::vector<Airplane> vp; // 同質airplanes集合 vp.push_back(airplane1); vp.push_back(airplane2); add_oil_to_aircrafts_in_the_sky(vp); // 爲airplanes進行「空中加油」 std::vector<Airship> vs; // 同質airships集合 vs.push_back(airship1); vs.push_back(airship2); add_oil_to_aircrafts_in_the_sky(vs); // 爲airships進行「空中加油」 }
咱們保留了類層次結構,目的是爲了可以利用run_vehicles()一致而優雅地處理異質對象集合vehicles的run問題。同時,利用函數模板 add_oil_to_aircrafts_in_the_sky<Aircraft>(),咱們仍然能夠處理特定種類的vehicles — aircrafts(包括airplanes和airships)的「空中加油」問題。其中,咱們避開使用指針,從而在執行性能和類型安全兩方面達到了預 期目標。
長期以來,C++社羣對於多態的內涵和外延一直爭論不休。在comp.object這樣的網絡論壇上,此類話題爭論至今仍隨處可見。曾經有人將動態多態(dynamic polymorphism)稱爲inclusion polymorphism,而將靜態多態(static polymorphism)稱爲parametric polymorphism或parameterized polymorphism。
我注意到2003年斯坦福大學公開的一份C++ and Object-Oriented Programming教案中明確提到了函數多態概念:Function overloading is also referred to as function polymorphism as it involves one function having many forms。文後的「參考文獻」單元給出了這個網頁連接。
可能你是第一次看到宏多態(macro polymorphism)這個術語。沒必要訝異 — 也許我就是造出這個術語的「第一人」。顯然,帶變量的宏(或相似於函數的宏或僞函數宏)的替換機制除了免除小型函數的調用開銷以外,也表現出了相似的多態性。在咱們上面的例子中,字符串相加所表現出來的符合直覺的鏈接操做,事實上是由底部運算符重載機制(operator overloading)支持的。值得指出的是,C++社羣中有人將運算符重載所表現出來的多態稱爲ad hoc polymorphism。
David Vandevoorde和Nicolai M. Josuttis在他們的著做C++ Templates: The Complete Guide一書中系統地闡述了靜態多態和動態多態技術。由於認爲「和其餘語言機制關係不大」,這本書沒有說起「宏多態」(以及「函數多態」)。(須要說明的是,筆者本人是這本書的繁體中文版譯者之一,本文正是基於這本書的第14章The Polymorphic Power of Templates編寫而成)
動態多態只須要一個多態函數,生成的可執行代碼尺寸較小,靜態多態必須針對不一樣的類型產生不一樣的模板實體,尺寸會大一些,但生成的代碼會更快,由於無需通 過指針進行間接操做。靜態多態比動態多態更加類型安全,由於所有綁定都被檢查於編譯期。正如前面例子所示,你不可將一個錯誤的類型的對象插入到從一個模板 實例化而來的容器之中。此外,正如你已經看到的那樣,動態多態能夠優雅地處理異質對象集合,而靜態多態能夠用來實現安全、高效的同質對象集合操做。
靜態多態爲C++帶來了泛型編程(generic programming)的概念。泛型編程能夠認爲是「組件功能基於框架總體而設計」的模板編程。STL就是泛型編程的一個典範。STL是一個框架,它提 供了大量的算法、容器和迭代器,所有以模板技術實現。從理論上講,STL的功能固然可使用動態多態來實現,不過這樣一來其性能必將大打折扣。
靜態多態還爲C++社羣帶來了泛型模式(generic patterns)的概念。理論上,每個須要經過虛函數和類繼承而支持的設計模式均可以利用基於模板的靜態多態技術(甚至能夠結合使用動態多態和靜態多態兩種技術)而實現。正如你看到的那樣,Andrei Alexandrescu的天才做品Modern C++ Design: Generic Programming and Design Patterns Applied(Addison-Wesley)和Loki程序庫已經走在了咱們的前面。