最近更新: 2007-07-19

C++和動態語言的泛型

cf 的回應 中提到了 polymorphism 和 generic 的看法。我的看法稍有不同。

我眼中的泛型(generic),若用非常簡化的方式來表達,就是不管型別,只看程式形式,亦即演算法。如稍候例舉的 max() 就是一種泛型演算法 (STL 也有一個同名的演算法)。我的說法和其他人的說法應該沒什麼不同。那麼多型(polymorphism)、泛型和樣板(templete)之間有什麼關係?端視程式語言的特性而定。

首先,多型(polymorphism)具有類別繼承的性質,是個體導向(OO)的概念,但泛型(generic) 不是個體導向概念。我們同樣可以在只有基礎資料型態,沒有繼承觀念的環境中使用泛型設計,只是彈性就低多了。在個體導向語言中,藉由類別繼承與動態連結的能力,才能充分發揮泛型設計的優點。

其次,說到泛型與樣板的關係。C/C++語言要求明確的型別,所以 C/C++ 如果沒有代換型別的能力,就實現不了泛型。C++ 樣板原意是「型別參數化」(這是C++之父最初提出樣板概念時的說法),意義表達很精確。所以C++要談泛型,要先從樣板著手。雖然樣板是在個體導向功能之後才加入 C++ 的功能,但它也可以先於個體導向之前存在。例如函數樣板(Function template) 就可以這樣使用。我們可以按傳統 C 那樣設計程式,但用函數樣板幫助程序員減少編寫重複的相似程式碼。精確地說,是將程式碼的產生工作由程序員轉給編譯器。

若從泛型與編譯器的關係來看,沒有編譯器的動態語言環境就不需要樣板的功能。See also: Ruby versus Smalltalk versus .... 所以 cf 說「我還沒有在其他的地方看過」是很合理的,因為 Ruby, Perl, JavaScript 等動態環境中用不著。故不能以「是否具備樣板功能」判定程式語言的泛型支援能力。

C 和樣板

以下範例,使用 C 的方式設計程式,且無視個體導向能力,僅運用樣板進行泛型設計。

使用樣板前

下列範例用到了 C99 後加入的 Overload 功能。如果不使用 Overload ,那麼函數名稱通常要分別寫成: max_i(), max_f(), max_c(), print_i(), print_f(), print_c() 等相似的名稱。那是更為傳統的寫法。

#include <stdio.h>

int max(int n, int m) {
    return (n >= m ? n : m);
}
// Overload max()
float max(float n, float m) {
    return (n >= m ? n : m);
}
// Overload max()
char max(char n, char m) {
    return (n >= m ? n : m);
}

void print(int n) {
    printf("%d\n", n);
}
// Overload print()
void print(float n) {
    printf("%f\n", n);
}
// Overload print()
void print(char n) {
    printf("%c\n", n);
}

int main() {
    float e = 2.321, f = 1.123;
    int i = 2, j = 1;
    char x = 'x', y = 'y';

    print(max(e, f));
    print(max(i, j));
    print(max(x, y));

    return 0;
}
使用 Template 後
#include <stdio.h>

template <typename var> var max(var n, var m) {
    return (n >= m ? n : m);
}

template <typename var> void print(var n) {
    printf("%d\n", n);
}
// specialized print()
void print(float n) {
    printf("%f\n", n);
}
// specialized print()
void print(char n) {
    printf("%c\n", n);
}

int main() {
    float e = 2.321, f = 1.123;
    int i = 2, j = 1;
    char x = 'x', y = 'y';

    print(max(e, f));
    print(max(i, j));
    print(max(x, y));

    return 0;
}

一個只用基本資料型態的 C 程式,在使用樣板進行泛型設計之後,程式碼內容成功地減肥了。

動態語言

我個人並非以語法區分動態語言和靜態語言,參閱《如何區分動態語言和靜態語言》。傳統上沒有編譯器的程式語言,如 Ruby, JavaScript, Perl, Python 等,我們目前慣稱為動態語言。這些程式語言的語意通常就有泛型概念。如下列 JavaScript 程式中的 max(),和 C++ 樣板相比,沒什麼不同。

function max(n, m) {
    return (n >= m ? n : m);
}

var e = 2.321, f = 1.123;
var i = 2, j = 1;
var x = 'x', y = 'y';

print(max(e, f));
print(max(i, j));
print(max(x, y));

從語法與語意看,動態語言的語法其實就是泛型的。它們只是因為沒有編譯動作,而不得不 late binding 。並不得不仰賴其他的處理策略。參考《Use C/C++ with Dynamic Languages is Easier Than Pure C++》及《個體之間協議互動行為的多種形式》。

動態語言之中,有些有編譯器的實作品,例如 JavaScript 。在 .Net Framework 中有一個 JScript compiler 。然而這些編譯器的實作功能殘缺不齊。它們充其量只能檢查語法,而不會檢查型態。如果廠商真的要實作 JavaScript 的編譯器,那麼應該要如同 C/C++ 那樣附帶一個宣告文件,或者是引用 TDD 概念要求附帶一個 Test case ,供編譯器檢查型態才對。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/3684523.html

樂多舊回應
leon740727@yahoo.com.tw(Leon) (#comment-11638645)
Fri, 20 Jul 2007 13:25:31 +0800
我覺得 cf 講的那一句「我還沒有在其他的地方看過」指的不只是您所謂的泛型耶…

因為 STL 作的並不只是代換型別,還包括 iteration, function object, algorithm…等等。也就是說,對同一個 vector,換一個 iteration,就換了一個走訪該容器的方式,程式邏輯都不用更改,如果再搭配適當的 function object,就可以在不更改主程式的狀況下,換掉整個巡訪的動作…

至少以我個人的所知來說,目前大部份的程式員還是以 foreach(); for (i=0; i
未留名 (#comment-11649671)
Fri, 20 Jul 2007 14:33:16 +0800
我所謂的泛型,若用非常簡化的方式來表達,就是不管型別,只看程式形式,亦即演算法。如上例中的 max() 就是一種泛型演算法 (STL 也有一個同名的演算法)。

我的說法和其他人的說法應該沒什麼不同吧?

我沒說泛型只是在作「代換型別」。然而C/C++語言要求明確的型別,所以 C/C++ 如果沒有代換型別的能力,就實現不了泛型。C++ 的 template 之原意是「型別參數化」(這是C++之父最初提出樣板概念時的說法),意義表達很精確。所以C++要談泛型,要先從 template 著手。

至於不要求明確提示資料型態的程式語言,在實現泛型能力的要求上,跟C++又有所不同了。像template 就用不著談。
jeffhung@ms44.url.com.tw(jeffhung) (#comment-14138027)
Tue, 07 Aug 2007 13:06:14 +0800
「C99 後加入的 Overload 功能」??

整份 C99 標準找不到任何一個 overload 字眼。
未留名 (#comment-22954102)
Thu, 27 Jun 2013 12:19:06 +0800
我想他的意思是他在程式裡用了這項技術