typeof 在 kernel 中的使用 —— C 語言的“編譯時多態”
C 語言本身沒有多態的概念,函數沒有重載的概念。然而隨著 C 語言編寫的軟件逐漸龐大,越來越多地需要引入一些其他語言中的特性,來幫助更高效地進行開發,Linux kernel 是一個典型例子。
在動態類型的語言里面,往往有 typeof 這種語法,來獲取變量的數據類型,比如 JavaScript 當中,typeof 以字符串型式返回了這個變量的數據類型,借由這種特性,往往可以根據傳入參數的類型不同,產生不同的行為。
GCC 提供的 typeof,實際上是在預編譯時處理的,最后實際轉化為數據類型被編譯器處理。用法上也和上述語言不太一樣。
基本用法是這樣的:
int a;
typeof(a) b; //這等同于int b;
typeof(&a) c; //這等同于int* c;
那么在內核中這種特性是怎樣使用的呢?
/** Check at compile time that something is of a particular type.* Always evaluates to 1 so you may use it easily in comparisons.*/
#define typecheck(type,x) \
({ type __dummy; \typeof(x) __dummy2; \(void)(&__dummy == &__dummy2); \1; \
})/** Check at compile time that 'function' is a certain type, or is a pointer* to that type (needs to use typedef for the function type.)*/
#define typecheck_fn(type,function) \
({ typeof(type) __tmp = function; \(void)__tmp; \
})
這兩段代碼來自于 include/linux/typecheck.h
,用于數據類型檢查。
宏 typecheck 用于檢查 x 是否是 type 類型,如果不是,那么編譯器會拋出一個 warning(warning: comparison of distinct pointer types lacks a cast);
而 typecheck_fn 則用于檢查函數 function 是否是 type 類型,不一致則拋出 warning(warning: initialization from incompatible pointer type)
。
原理很簡單,對于 typecheck ,只有當 x 的類型與 value 一致,&__dummy == &__dummy2
的比較才不會因為類型不匹配而拋出 warning ,詳情可以參考 C 語言對于指針操作的標準規定。對于 typecheck_fn ,當然也只有 function 的返回值和參數表與 type 描述一致,才不會因為類型不匹配而拋出 warning 。
到這里有人可能會有一個疑問,內核代碼里執行類型檢查會不會降低效率?答案是不會的,因為實際上,這些為類型檢查而聲明的臨時變量,實際上在上下文中都沒有使用,并且還特別地強制類型轉換為 void 防止任何由這些臨時變量產生的結果被使用的情況,因此在編譯器優化時,就將這些無用的代碼刪除了。
然后 kernel 中還定義了使用另一種類型檢查策略的獲取最大最小值的宏。
/** ..and if you can't take the strict* types, you can specify one yourself.** Or not use min/max/clamp at all, of course.*/
#define min_t(type, x, y) ({ \type __min1 = (x); \type __min2 = (y); \__min1 < __min2 ? __min1: __min2; })#define max_t(type, x, y) ({ \type __max1 = (x); \type __max2 = (y); \__max1 > __max2 ? __max1: __max2; })
這個例子里面不要求 x 和 y 是嚴格等于 type 類型,只要 x 和 y 能夠安全地完成隱式類型轉換為 type 就可以安全通過編譯,否則會拋出 warning。
另外一個非常經典的例子就是交換變量。
/** swap - swap value of @a and @b*/
#define swap(a, b) \do { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } while (0)
試想如果沒有 typeof,要怎么在 C 語言中實現這種類似 C++ 模板的特性呢?
這里還有一個功能和 typeof 類似的運算符: typeid
typeid 是為 RTTI(運行時類型檢查) 提供的第二個運算符。
-
typeid 運算符允許在運行時確定對象的類型
-
返回的結果是 const type_info&
-
typeid 運算符在應用于多態類類型的左值時執行運行時檢查,其中對象的實際類型不能由提供的靜態信息確定;
-
typeid 也可以在模板中使用以確定模板參數的類型
-
typeid 是操作符,不是函數,運行時獲知變量類型名稱;
和 typeof 的主要區別有二:
- typeof(編譯器提供) 是一個編譯時結構,并返回編譯時定義的類型,和 C++11 提供的關鍵字 decltype 類似
- typeid( C++ 提供) 是一個運行時結構,因此提供了有關該值的運行時類型的信息
typeid 表達式的形式是 typeid(e), 其中 e 可以是任意表達式或者類型的名字。 typeid 操作的結果是一個常量對象的引用,該對象的類型是標準庫類型 type_info 或者 type_info 的公有派生類型。如果表達式是一個引用,則 typeid 返回該引用所引對象的類型。不過當 typeid 作用于數組或函數時,并不會執行向指針的標準類型轉換。也就是說,如果我們對數組 a 執行 typeid(a) ,則所得的結果是數組類型而非指針類型。
當運算對象不屬于類類型或者是一個不包含任何虛函數的類時, typeid 運算符指示的運算對象的靜態類型。而當運算對象是定義了至少一個虛函數的類的左值時, typeid 的結果直到運行時才會求得。
當 typeid 作用于指針時(而非指針所指的對象),返回的結果是該指針的靜態編譯時類型
typeid 是否需要運行時檢查決定了表達式是否會被求值。只有當類型含有虛函數時,編譯器才會對表達式求值。反之,如果類型不含有虛函數,則 typeid 返回表達式的靜態類型;編譯器無須對表達式求值也能知道表達式的靜態類型。
RTTI 栗子:
class Base {friend bool operator==(const Base&, const Base&);
public:// Base 的接口成員
protected:virtual bool equal(const Base&) const;// Base 的數據成員和其他用于實現的成員
};class Derived : public Base {
public:// Derived 的其他接口成員
protected:bool equal(const Base&) const;// Derived 的數據成員和其他用于實現的成員
};// 類型敏感的相等運算符
// 接下來介紹我們是如何定義整體的相等運算符的:
bool operator==(const Base &lhs, const Base &rhs) {// 如果 typeid 不相同,返回 false;否則虛調用 equalreturn typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
在這個運算符中,如果運算對象的類型不同則返回 false。否則,如果運算對象的類型相同,則運算符將其工作委托給虛函數 equal 。當運算對象是 Base 的對象時,調用 Base::equal ;當運算對象是 Derived 的對象時,調用 Derived::equal 。
虛 equal 函數
繼承體系中的每個類必須定義自己的 equal 函數。派生類的所有函數要做的第一件事都是相同的,那就是將實參的類型轉換為派生類類型:
bool Derived::equal(const Base &rhs) const {// 我們清楚這兩個類型是相等的,所以轉換過程不會拋出異常auto r = dynamic_cast<const Derived&>(rhs);// 執行比較兩個 Derived 對象的操作并返回結果
}
上面的類型轉換永遠不會失敗,因為畢竟我們只有在驗證了運算對象的類型相同之后才會調用該函數。然而這樣的類型轉換必不可少,執行了類型轉換后,當前的函數才能訪問右側運算對象的派生類成員
type_info 類
type_info 的操作
-
t1 == t2 如果 type_info 對象 t1 和 t2 表示同一種類型,返回 true, 否則返回 false
-
t1 != t2 與上一條相反
-
t.name() 返回一個 C 風格的字符串,表示類型名字的可打印形式。類型的名字生成方式因系統而異
-
t1.before(t2) 返回一個 boo 值,表示 t1 是否位于 t2 之前。 before 所采用的順序關系是依賴于編譯器的。
一般 type_info 是作為一個基類出現,所以應該提供一個公有的虛析構函數。當編譯器希望提供額外的類型信息時,通常在 type_info 的派生類中完成。
type_info 類沒有默認構造函數,而且他的拷貝和移動構造函數以及賦值運算符都被定義成刪除的。因此我們無法定義或拷貝 type_info 類型的對象,也不能為 type_info 類型的對象賦值。創建 type_info 對象的唯一途徑是使用 typeid 運算符。
栗子:
int arr[10];
Derived d;
Base *p = &d;std::cout << typeid(42).id() << ", "<< typeid(arr).name() << ", "<< typeid(Sales_data).name() << ", "<< typeid(std::string).name() << ", "<< typeid(p).name() << ", "<< typeid(* p).name() << std::endl;
在作者的計算機上運行該程序,結果如下
i, A10_i, 10Sales_data, Ss, P4Base, 7Derived