名稱
用具體類型代替void指針
動機
最近有人問我c++有沒有一種方法可以讓一個數(shù)組包含不同類型的元素。我的第一反應(yīng)是他python寫得多了。不過轉(zhuǎn)念一想,靜態(tài)語言C/C++也是有解的。
一個常見的方法是利用C/C++弱類型的特點,將數(shù)組類型退化為void *:
struct A a = {1, 2};
void *voids[3];
voids[0] = 1;
voids[1] = "abc";
voids[2] = &a;
printf("#0:%d, #1:%s, #2:%d-%d\n", (int)voids[0], (char *)voids[1],
((struct A*)voids[2])->a, ((struct A*)voids[2])->b);
退化的意思有兩個:
從
voids取出東西,編譯器不知道這東西是什么。于是需要人肉翻譯器(也就是你)強轉(zhuǎn)成正確的類型。至于強轉(zhuǎn)的類型是不是正確的,編譯器愛莫能助,畢竟C語言的哲學(xué)之一就是程序員知道自己在做什么并且總是對的。但是強轉(zhuǎn)多了也會有陰溝翻船的時候,實際上程序員不一定是對的,特別是把這個當(dāng)做模塊間的接口時。-
另外一個問題是編譯告警。
voids[0] = 1;用gcc 6.2.0編譯會產(chǎn)生編譯告警,提示賦值時將整數(shù)賦給指針,未作類型轉(zhuǎn)換 [-Wint-conversion]。加上強轉(zhuǎn)(voids[0] = (void *)1;)可以消除這個告警。但是
printf那行的(int)voids[0]還有編譯告警:將一個指針轉(zhuǎn)換為大小不同的整數(shù) [-Wpointer-to-int-cast]。編譯環(huán)境是X64,int長度是32位,而指針長度是64位。這也算不上難題,可以把類型改成long來解決。還得同步把%d改成%ld,不然又會報另外一個告警。說了這么大堆編譯告警,也許有點吹毛求疵。但是也可以看出整型和指針其實是不相容的,這種寫法多少顯得有點格格不入。
當(dāng)然也可以把int換成int *,這樣大家都是指針。這相當(dāng)于增加一層薄薄的中間層,開銷值不值得尚且不論,單單是這個指針的生命周期管理也得好好想想。
如果前面兩個問題尚且還可以忍受,稍有不慎就會拋異?!@點相信大家都接受不了。究其原因,這個方法在setter和getter之間形成了一個關(guān)于類型的契約,不過這個契約卻是若有若無。因為人為跳過了靜態(tài)語言的一大優(yōu)勢--類型檢查,只能依賴不靠譜的人肉編譯器(沒錯,還是你)來檢查。當(dāng)脆弱的契約在不經(jīng)意間被打破,BUG會在運行時伺機而動,有時甚至?xí)摲饋?等待在關(guān)鍵時刻給你當(dāng)頭棒喝。
如下代碼,編譯能通過,甚至都沒有編譯告警,但是在運行時卻拋出了一個段錯誤。
voids[0] = (void *)1;
int x = *((int *)voids[0]);
在代碼初期,這類問題一般很少發(fā)生。隨著新需求帶來的增量開發(fā),架構(gòu)腐化,代碼由原作者轉(zhuǎn)交其他人維護,這類問題逐漸顯現(xiàn)出來。
機制
void指針數(shù)組的優(yōu)勢在于前期寫代碼很方便,但是成本卻悄悄的累積到后期的維護工作中。但是對于大型軟件來說,前期開發(fā)只占整個軟件生命周期很小一部分,后期維護占絕大部分。所以這個方案并不可取。這是一種短視的行為,對長期的成本卻視而不見。這是一種常見的人性的弱點。正如一個大牛所說的『開發(fā)有時候是反人性的』,只有不斷的克服人性的弱點,才能開發(fā)出優(yōu)秀的軟件。寧可開始慢一些,也要開發(fā)出可持續(xù)演進的代碼。
最簡單直接的方案是把void指針數(shù)組替換成結(jié)構(gòu)體,每個類型對應(yīng)一個結(jié)構(gòu)體成員。例如,前面的void指針數(shù)組可以改為下面的結(jié)構(gòu)體:
struct B {
int a;
char *b;
struct A c;
};
C++11引入了tuple,可以類似數(shù)組一樣使用:
struct A a = {1, 2};
auto t = std::make_tuple(1, "abc", a);
auto ta = std::get<2>(t);
std::cout << "#1:" << typeid(std::get<0>(t)).name() << "/" << std::get<0>(t) << "\n"
<< "#2:" << typeid(std::get<1>(t)).name() << "/" << std::get<1>(t) << "\n"
<< "#3:" << typeid(ta).name() << "/" << ta.a << "-" << ta.b << std::endl;
輸出為:
#1:i/1
#2:PKc/abc
#3:1A/1-2
C++17還把any轉(zhuǎn)正了,any+vector用起來很方便:
std::vector<std::any> v;
v.push_back(1);
v.push_back("abc");
v.push_back(a);
std::cout << "#1:" << v[0].type().name() << "/" << std::any_cast<int>(v[0]) << "\n"
<< "#2:" << v[1].type().name() << "/" << std::any_cast<const char *>(v[1]) << "\n"
<< "#3:" << v[2].type().name() << "/" << std::any_cast<A>(v[2]).a << std::endl;
但是any方案也不是很完美。一方面是因為取用還是要用any_cast強轉(zhuǎn),另一方面,編譯時不會報錯。
例如下面代碼:
std::any_cast<float>(v[0]);
編譯時沒有報出錯誤,而是在運行時拋出異常:
terminate called after throwing an instance of 'std::bad_any_cast'
what(): bad any_cast
any只能算作void *的運行時安全版本(運行時檢查類型)。
而C++17引入的另一個新類型variant則是枚舉類型的運行時安全版本。variant也存在any的兩個問題:
- 獲取時也需要指定類型(
std::get<類型>); - 如果類型不對,也會在運行時拋一個異常(
std::bad_variant_access),而不是編譯告警。
C++如果用基類指針數(shù)組,在運行時通過type_info進行類型檢查,可參考cppmock的實現(xiàn)。