1. 程式人生 > >c++的靜態多型和動態多型(筆記)

c++的靜態多型和動態多型(筆記)

多型(polymorphism)一詞最初來源於希臘語polumorphos,含義是具有多種形式或形態的情形。在程式設計領域,一個廣泛認可的定義是“一種將不同的特殊行為和單個泛化記號相關聯的能力”。和純粹的面向物件程式設計語言不同,C++中的多型有著更廣泛的含義。除了常見的通過類繼承和虛擬函式機制生效於執行期的動態多型(dynamic polymorphism)外,模板也允許將不同的特殊行為和單個泛化記號相關聯,由於這種關聯處理於編譯期而非執行期,因此被稱為     靜態多型(static polymorphism)。
    事實上,帶變數的巨集和函式過載機制也允許將不同的特殊行為和單個泛化記號相關聯。然而,習慣上我們並不將它們展現出來的行為稱為多型(或靜態多型)。今天,當我們談及多型時,如果沒有明確所指,預設就是動態多型,而靜態多型則是指基於模板的多型。不過,在這篇以C++各種多型技術為主題的文章中,我們首先還是回顧一下C++社群爭論已久的另一種“多型”:函式多型(function polymorphism),以及更不常提的“巨集多型(macro polymorphism)”。
C++支援多種風格的程式設計模式稱之為程式設計範型 C++支援的程式設計範型包括面向過程的基於物件的 面向物件的和泛型程式設計 通過指標和引用來支援多型是面向物件的程式設計範型區
別於基於物件的程式設計範型的本質所在 所謂多型  是指通過單一的標識支援不同的特定行為的能力 C++支援多種形式的多型 從繫結時間來看可以分成靜態多型和動態多型也稱為編譯期多型和執行期多型 從表現的形式來看 有虛擬函式 模板過載和轉換.由於靜態多型在時間和空間上都比動態多型表現得好.因此在其他的條件相同的情況下 應該更多地使用

靜態多型

函式多型

也就是我們常說的函式過載(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)分別被替換為兩個整數相加和兩個字串相加的具體表達式。整數相加體現為求和,而字串相加則體現為連線(注:string.h庫已經過載了。程式的輸出結果符合直覺:
1 + 2 = 3
Hello, + world! = Hello, world!

動態多型

這就是眾所周知的的多型。現代面嚮物件語言對這個概念的定義是一致的。其技術基礎在於繼承機制和虛擬函式。例如,我們可以定義一個抽象基類Vehicle和兩個派生於Vehicle的具體類CarAirplane

// 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()
或許動態多型最吸引人之處在於處理異質物件集合的能力:

// 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類層次結構,相反,我們編寫彼此無關的具體類CarAirplane(它們都有一個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++應用中,我們可能需要結合使用動態多型和靜態多型兩種機制,以期達到物件操作的優雅、安全和高效。例如,我們既希望一致而優雅地處理vehiclesrun問題,又希望安全而高效地完成給飛行器(飛機、飛艇等)進行空中加油這樣的高難度動作。為此,我們首先將上面的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()一致而優雅地處理異質物件集合vehiclesrun問題。同時,利用函式模板add_oil_to_aircrafts_in_the_sky<Aircraft>(),我們仍然可以處理特定種類的vehicles — aircrafts(包括airplanesairships)的空中加油問題。其中,我們避開使用指標,從而在執行效能和型別安全兩方面達到了預期目標。

結語

長期以來,C++社群對於多型的內涵和外延一直爭論不休。在comp.object這樣的網路論壇上,此類話題爭論至今仍隨處可見。曾經有人將動態多型(dynamic polymorphism)稱為inclusion polymorphism,而將靜態多型(static polymorphism)稱為parametric polymorphismparameterized 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 VandevoordeNicolai M. Josuttis在他們的著作C++ Templates: The Complete Guide一書中系統地闡述了靜態多型和動態多型技術。因為認為和其他語言機制關係不大,這本書沒有提及巨集多型(以及函式多型)。(需要說明的是,筆者本人是這本書的繁體中文版譯者之一,本文正是基於這本書的第14The Polymorphic Power of Templates編寫而成)

動態多型只需要一個多型函式,生成的可執行程式碼尺寸較小,靜態多型必須針對不同的型別產生不同的模板實體,尺寸會大一些,但生成的程式碼會更快,因為無需通過指標進行間接操作。靜態多型比動態多型更加型別安全,因為全部繫結都被檢查於編譯期。正如前面例子所示,你不可將一個錯誤的型別的物件插入到從一個模板例項化而來的容器之中。此外,正如你已經看到的那樣,動態多型可以優雅地處理異質物件集合,而靜態多型可以用來實現安全、高效的同質物件集合操作。

靜態多型為C++帶來了泛型程式設計(generic programming)的概念。泛型程式設計可以認為是元件功能基於框架整體而設計的模板程式設計。STL就是泛型程式設計的一個典範。STL是一個框架,它提供了大量的演算法、容器和迭代器,全部以模板技術實現。從理論上講,STL的功能當然可以使用動態多型來實現,不過這樣一來其效能必將大打折扣。

靜態多型還為C++社群帶來了泛型模式(generic patterns)的概念。理論上,每一個需要通過虛擬函式和類繼承而支援的設計模式都可以利用基於模板的靜態多型技術(甚至可以結合使用動態多型和靜態多型兩種技術)而實現。正如你看到的那樣,Andrei Alexandrescu的天才作品Modern C++ Design: Generic Programming and Design Patterns AppliedAddison-Wesley)和Loki程式庫已經走在了我們的前面。