一些瞠目結舌的 C/C++ 程式碼技巧

發表於2016-09-23

【導讀】:說到 C/C++ 程式碼技巧,也許會有童鞋說 #define true false,這是屬於 C/C++ 程式設計師離職前惡搞之類的抖機靈。即便想,也不能幹。別忘了有這樣一句程式設計名言:「在編寫程式碼的時候,你要經常想著,那個最終維護你程式碼的人可能將是一個有暴力傾向的瘋子,並且他還知道你住在哪裡。」

本文整理了兩位知乎網友對《你見過哪些令你瞠目結舌的C/C++程式碼技巧?》的回覆分享,均已獲授權。

一、陳宇飛的分享,218 頂

寫幾個folly(Facebook開源的c++庫)裡面的例子。

——四更,folly::future 是如何chain你的callback的 ——

folly::future 是一個酷炫屌炸天的庫,FB內部大量的非同步C++的程式碼都是基於future的。我這段只講他怎麼支援下列語法的:

也就是說,callback B 是接在callbackA後面的話,callback A 如果返回的是 T,我們可以支援callback B接受 T&&, T&, T, Try<T>, Try<T>&& 廢話不說先上程式碼。解釋在程式碼後面,所以嫌程式碼長的可以直接滑過去看解釋。

這裡一大波template我們一個一個來。

可以無視掉,你可以想象成FF就是F,std::declval 讓你把 F變成 F&&,所以可以用

這樣的語法拿到F(Args args…) 的返回值,不管F是object還是lambda。這樣,resultOf可以拿到我們的callback的返回type。現在我們得把這個返回值跟下一個函式的argument對應起來。這裡我們用callableWith

這裡check有兩個specialization,一個在編譯時候會返回true一個會返回false。注意只要不符合第一個specialization的都是false,也就是說resultOf沒有成功,check(nullptr) 就是false type。這個技巧叫做Substitution Failure Is Not An Error SFINAE – cppreference.com 。再配倒數第二行的typedef,如果 F可以接受args,那麼callableWith<F>(Args args…)::value == true type。

最後,把所有我們允許的類用std::conditional一個一個試過去

我們就可以在編譯時間確保我們可以支援我們所有想支援的7個類啦。那具體拿著第一個callback的返回值怎麼傳輸到第二個callback上面做argument呢?這一段變種太多,我只給大家看最簡單的變種:

core_ 是future 的member,也就是說我們設定的callback僅僅只是被加到callback裡面去了,還沒有被執行。只有當你執行future.get() 的時候值才會被你拿到

啊,所有的callback執行都被扔到baton裡面去了,我先不在這裡繼續深挖了。總之

  • 在編譯的時候,是通過一系列declval/decltype/SFAINE/std::conditional 來確保類是對的;
  • 在執行的時候 cb1被傳到future.core_ -> baton -> 拿到值 -> 根據cb2是不是接受try來分叉 -> 值扔給 cb2

我把Promise/Baton的內容全部跳過去了,因為要把那兩個也講了就沒完沒了了,在template metaprogramming上面也沒有future這麼fancy。以後有機會再細寫那兩個庫吧!

——–三更,講點稍微實用一點的資料結構吧, 兩個看起來風馬牛不相及其實儲存上一致的folly::Optional 和 folly::Indestructible ——

folly::Optional
folly/Optional.h at master · facebook/folly · GitHub

C++裡面不是所有的類都可以是null的,特別有的時候這個類是其他人硬塞給你的。而當你需要它可是你null的時候,你把這個類放到folly optional裡面,它就可以是null啦。folly optional裡面比較有意思的是它儲存的機制。

StorageTriviallyDestructible 還稍微合理一點,StorageNonTriviallyDestructible.clear() 裡面~Value() 絕對很少見。這是c++11裡面unconstrained union的新玩法,因為在unconstrained union裡面你必須有能力可以銷燬一個non-POD類,所以c++語法開放了~Value() 這種語法,讓你可以銷燬這個值。這個功能在這裡就被弄出了新玩法,被用來支援folly::Optional.clear(),這樣就算是一個NonTriviallyDestructible的物件你也可以隨時銷燬它。

這種unconstrained union的啟動機制也是比較麻煩的。folly::optional.set 長這樣:

這個叫placement new,就是說你給new 一個地址,new直接在你給的地址上面initialize,而不是去heap裡面佔記憶體。有上面兩個玩法的話,你就可以隨時隨地在c++啟動,銷燬這個值啦!

folly::Indestructible

如何確保你的meyer’s singleton永遠不死?這樣儲存你的類:

看起來好像沒有什麼特殊的對不對?不要忘記這個T在這裡肯定是個non trivially destructable的類。在這個union裡面,既然你的destructor是空的,那麼也就是說value永遠被遺忘了。。。遺忘了。。。遺忘了。。

看到這裡有人要開罵了,為什麼不直接new一個值出來,不銷燬就好了?這跟new一個新的值出來最大的差別是這個不可以被遺忘的值是可以被inline的,它用的記憶體不是heap裡面的記憶體(至少value本身不在heap上面)。這在效率上的差別是不可小覷的。

folly::optional 跟 folly::indestructable 都是利用了新標準裡面union的新特性。看來新玩法還是要多想

———謝謝各位踴躍點贊,這段是二更。要是過一千的話我就寫MPMCQueue哦——–
f

olly::Conv 是可以把所有類轉化成所有類的庫。也不是所有啦,不過正常人用的到的都有。我這裡只講int轉字串,float/double 實在太麻煩。

先看數字轉字串裡面算多少位數的

下面那個for loop多友好,上面x86那段是什麼鬼!

builtin_clzll 是gcc裡面的一個函式,具體定義看這裡Other Builtins 簡單來說就是算有幾個開頭的0的。這裡面的0是2進位制的0,所以63-leading zeros就是說二進位制裡面有幾位數。

這個就是亮點了。高中數學沒學好的話,這裡強調一下

  • log_10(v) =log_10(2) * log_2(v) (The Change-of-Base Formula) 。
  • >> 8 就是除以以256
  • bits在這裡已經是log(2)了
  • +1是因為 0.3010略小於77/256我們繼續瞎,現在開始正式轉換

這裡面開始甩說用pos慢了,估計原因是loop unrolling做不了,但是具體不好說,我得問問他。

peephole optimization簡單來說就是一段短小精悍的程式碼可以被compiler 變得更短小精悍(Peephole optimization),具體肯定也是實測過才敢拿出來講。非常好讀懂我就不BB了。不過用C++用的熟練的看到這種不差buffer大小的肯定非常不爽。為什麼可以不查?!

好了我服了。。。

———————–原先的答案——————————
AtomicStruct
folly/AtomicStruct.h at master · facebook/folly · GitHub

類似於std::atomic, 但是任何小於8個byte的POD類都可以變成atomic的。實現的方法如下:
用一個unconstrained union 來存資料:

T是你的類,Atom 就是std::atomic,Raw是這麼來的

我看到這裡已經開始瞎了。compare exchange是這樣的

裡面的encode/decode就是拿來騙編譯器的memcpy。寫了這麼多廢話,說白了就是為了讓編譯器開心的可以用各種std::atomc<int>DiscriminatedPtr

用法就是boost::variant,但是用DiscriminatedPtr沒有任何多餘的代價,就是一個指標的大小。為什麼可以沒有代價呢?應為64位系統裡面其實只有48位拿來做地址了,剩下16位是沒有被系統用起來的。所以要地址是這麼讀的

那前16個bit是存什麼呢?存的是現有這個類的index。每次存的時候,會通過index找到對應的類

然後

那typeIndex是什麼鬼!?typeIndex是一個編譯是通過遞迴製造出來的列表,可以在編譯時製造出一個數字對應類的列表

具體實現在這裡 folly/DiscriminatedPtrDetail.h at master · facebook/folly · GitHub。 這樣在編譯時間你就可以知道你要的類是不是這個指標支援的類。要是對編譯時的黑魔法感興趣的話,可以從boost的index_sequence看起 boost/fusion/support/detail/index_sequence.hpp
DiscriminatedPtr還支援visitor pattern,具體這裡不細講應為沒有什麼typeIndex以外的黑科技。具體用法可以參照boost::invariant Tutorial – 1.61.0先寫著麼多,要是有人看的話我就繼續寫。你們可得用力點贊啊!

二、Shiky Chang 回答,1225 頂

如果說「瞠目結舌」的話,IOCCC 上隨便拿一篇獲獎程式碼出來就足以讓人下巴落地了。

The International Obfuscated C Code Contest

一個比較經典的例子是 1988 年得獎的程式碼,這個程式直接估算字元面積求圓周率,可讀性算是比較友好的:

•westley.c•

注:這段程式實際上是 1989 年修正過的,由於 88 年原來程式程式碼沒有考慮到 ANSI C 的編譯標準,導致在處理例如

的時候,老舊的 K&R 框架和 ANSI C 結果不一樣:K&R 是直接的

而 ANSI C 編譯結果實際上等同於

因此之前的程式現在執行的話出來的結果是 0.250,而不是 3.141。修正過的程式就沒有這個問題了。

又比如 13 年有個只有一行的程式,可以判斷從 Franklin Pierce 往後的 31 位美國總統是民主黨還是共和黨,這個就有點不知所云了:

•cable1.c•

使用方法:

總統名要小寫,republican 和 democrat 順序不能顛倒。

@chua zier 提醒,歷史上的確有重名的美國總統,除了 Johnson 之外,還有 Theodore Roosevelt / Franklin D. Roosevelt,程式原作者註明用

表示 Theodore Roosevelt,而用

表示 Franklin D. Roosevelt。

這一行程式碼做了這麼多事:首先查詢輸入的總統的名字,然後在一個 look-up table 裡面找出對應的政治陣營,再輸出出來。問題在於這 31 位總統名字存放在哪裡?而這個 look-up table 又存放在哪裡?

有趣的是,IOCCC 的評委還提到,你甚至可以用這個程式檢測一些 IT 大佬的 Mac / PC 陣營:

難道這個程式暴露了 Ballmer 離開微軟的真相?

最近幾屆比賽的程式碼為了增加混亂程度,程式碼越來越長,可讀性也越來越差(不過話說回來,讓可讀性變得越來越差其實原本就是這個比賽的第一宗旨吧),不少程式碼甚至本身就是個 ASCII artwork……比如 11 年有一隻阿卡林:

•akari.c•

一些瞠目結舌的 C/C++ 程式碼技巧

為了保持美觀我就直接上圖了。原始碼見此:ioccc.org/2011/akari/ak

–––––––––– !!! 前方阿卡林軍團高能預警 !!! ––––––––––

這個阿卡林程式實際上是一個影象 down-sampler,可以接受符合條件的 PGM / PPM 灰度影象或者 LF 換行(不支援 CR-LF)的 ASCII art 為輸入,然後轉換輸出一個處理後的影象 / ASCII art。不過這個阿卡林最逆天的地方在於,它可以用自身的原始碼文字作為輸入,輸出生成另一個可以編譯執行的程式的程式碼!而且把這個生成的程式文字繼續作為輸入做進一步 down-sample,又可以生成一段可以編譯的程式碼,如此反覆,可以套多達4層!詳細的食用方法如下:

然後生成的阿卡林·2號是這個樣子的:

•akari2.c•

一些瞠目結舌的 C/C++ 程式碼技巧

看不清?請摘下眼鏡或者退遠了看。注意,阿卡林·2號也是可以編譯執行的,她的功能是把輸入的 ACSII 文字的每個字元中間插入空格以及每行之間插入空行,生成一段“疏鬆”了的文字。我們用阿卡林·2號自己做實驗品

成功了!生成了一隻阿卡林·2號·舒鬆

•akari2fat.txt•

一些瞠目結舌的 C/C++ 程式碼技巧

阿卡林·2號還能幹別的,她支援一個 rot13 引數:

生成的是經過 ROT13 仿射變換的文字,我們稱之為阿卡林·2號·舒鬆·加蜜吧!

但是還沒完……如果我們把原版阿卡林放進去再來一層呢?

於是阿卡林·3號誕生:

•akari3.c•

可憐的阿卡林·3號,由於“馬賽克”(down-sample)次數太多,摘了眼鏡也只能模糊看到一點點……我們來問問阿卡林·3號對於誕生的感受吧:

於是她回答:

居然會說ゆるゆり!

最後,我們嘗試生產一下阿卡林·4號

•akari4.c•

順利生產!雖然內容已經直截了當了,不過我們還是採訪一下她吧:

她的答覆是:

至此,阿卡林軍團全部誕生!

一些瞠目結舌的 C/C++ 程式碼技巧

不得不佩服作者構建程式碼的精妙程度。他的個人主頁在這裡:uguu… (這位作者其實已經是這比賽的常客了,先後一共拿過 6 次不同的獎項。)

@馬琦明 提醒,我又把上面這位作者的另一個作品搬出來了,13 年的 Most Catty——炮姐程式。這程式的程式碼長這個樣子:

•misaka.c•

一些瞠目結舌的 C/C++ 程式碼技巧

原始碼:ioccc.org/2013/misaka/m

對的,當你看到原來是這個“御阪”的時候,你就知道,我們要開始造(kè)人(lóng)了……

這個御阪的作用是把輸入的 ASCII 橫向連線起來。首先連線兩個自己試試:

“把兩個御阪輸入一個御阪,會生成什麼?”“兩個御阪。”

•misaka2.c•

一些瞠目結舌的 C/C++ 程式碼技巧

聽起來很不可思議但是在這位作者的構建下完全不出意外地,上面這個御阪-2 居然也是可以編譯執行的:

御阪-2 的功能是把輸入的 ASCII 縱向連線起來。那我們就試著縱向連線兩個御阪:

於是御阪-3 誕生了:

•misaka3.c•

一些瞠目結舌的 C/C++ 程式碼技巧

我們來執行一下這個御阪-3。你此時腦中的景象可能是這樣的:

一些瞠目結舌的 C/C++ 程式碼技巧

但是你錯了,御阪-3 會給你造出來更加精神汙染的那隻 long cat:

一些瞠目結舌的 C/C++ 程式碼技巧

沒錯就是這隻喵:

一些瞠目結舌的 C/C++ 程式碼技巧

這裡其實有 Unix 的 cat 指令的梗……如果之前你在執行御阪-2 的時候,用了更多的御阪作為輸入,例如 4 個:

那麼御阪-4 會給你造一隻更長的 looooong cat:

一些瞠目結舌的 C/C++ 程式碼技巧

按作者的意思,你可以最多疊加 31 個御阪來生成一隻 looo….ooong cat(具體上限由編譯器的 sizeof(int) 決定)。

13 年還有浙大教授侯啟明寫的 ray tracer 程式,雖然程式碼本身存在爭議是否符合比賽規則,例如為避免長度超限制而使用了一些壓縮方法、程式是個死迴圈。如果這段程式可讀性不是這麼噁心的話其實還是非常值得鑽研的,裡面用到了很多有趣的資料結構和著色體系。

食用方法也很簡單,把程式掛在那兒跑一晚上,強制退出,就可以看結果了。由於是無窮盡的遞迴,程式跑的時間越長,影象就越精緻。詳細的說明和原始檔還是參考官網咖:

Previous IOCCC Winners with spoilers

這裡有個示例圖。

一些瞠目結舌的 C/C++ 程式碼技巧

侯老師還有另外三個作品上榜,一個是極其酷炫的 syntax highlightener,還有一個(原始碼本身就是 GUI 的)科學計算器,後面這個已經有人 @paintsnow 回答過了。最新一個是上個月剛剛公佈的新一屆獲獎作品 MD5 without integers,但是這個的原始碼還沒有公佈,估計要等到明年了。

三、平方根倒數速演算法

伯小樂看到有其他網友提到了平方根倒數速演算法

這個技巧就不詳細解釋了,推薦大家重新再看看這篇文章《數學之美:平方根倒數速演算法中的神奇數字 0x5f3759df 》。

相關文章