用具體類型代替void指針

名稱

用具體類型代替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)于增加一層薄薄的中間層,開銷值不值得尚且不論,單單是這個指針的生命周期管理也得好好想想。

如果前面兩個問題尚且還可以忍受,稍有不慎就會拋異?!@點相信大家都接受不了。究其原因,這個方法在settergetter之間形成了一個關(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)

例子

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容