【導讀】:說到 C/C++ 程式碼技巧,也許會有童鞋說 #define true false
,這是屬於 C/C++ 程式設計師離職前惡搞之類的抖機靈。即便想,也不能幹。別忘了有這樣一句程式設計名言:「在編寫程式碼的時候,你要經常想著,那個最終維護你程式碼的人可能將是一個有暴力傾向的瘋子,並且他還知道你住在哪裡。」
本文整理了兩位知乎網友對《你見過哪些令你瞠目結舌的C/C++程式碼技巧?》的回覆分享,均已獲授權。
一、陳宇飛的分享,218 頂
寫幾個folly(Facebook開源的c++庫)裡面的例子。
——四更,folly::future 是如何chain你的callback的 ——
folly::future 是一個酷炫屌炸天的庫,FB內部大量的非同步C++的程式碼都是基於future的。我這段只講他怎麼支援下列語法的:
1 2 3 4 5 6 7 8 9 10 |
folly::makeFuture().then([]() { // 返回一個數字 return 10; }).then([](int i) { // 如果寫 }).then([](string i) { 的話編譯器會報錯 // 但是寫 }).then([](Try<int> i) { 的話編譯器不會報錯 // }).then([](int&& i) { 的話編譯器也不會報錯 // }).then([](Try<int> && i) { 的話編譯器也不會報錯 }); |
也就是說,callback B 是接在callbackA後面的話,callback A 如果返回的是 T,我們可以支援callback B接受 T&&, T&, T, Try<T>, Try<T>&& 廢話不說先上程式碼。解釋在程式碼後面,所以嫌程式碼長的可以直接滑過去看解釋。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
/** When this Future has completed, execute func which is a function that takes one of: (const) Try<T>&& (const) Try<T>& (const) Try<T> (const) T&& (const) T& (const) T (void) Func shall return either another Future or a value. A Future for the return type of func is returned. Future<string> f2 = f1.then([](Try<T>&&) { return string("foo"); }); The Future given to the functor is ready, and the functor may call value(), which may rethrow if this has captured an exception. If func throws, the exception will be captured in the Future that is returned. */ template < typename F, typename FF = typename detail::FunctionReferenceToPointer<F>::type, typename R = detail::callableResult<T, FF>> typename R::Return then(F&& func) { typedef typename R::Arg Arguments; return thenImplementation<FF, R>(std::forward<FF>(func), Arguments()); } template<typename F, typename... Args> struct callableWith { template<typename T, typename = detail::resultOf<T, Args...>> static constexpr std::true_type check(std::nullptr_t) { return std::true_type{}; }; template<typename> static constexpr std::false_type check(...) { return std::false_type{}; }; typedef decltype(check<F>(nullptr)) type; static constexpr bool value = type::value; }; template<typename T, typename F> struct callableResult { typedef typename std::conditional< callableWith<F>::value, detail::argResult<false, F>, typename std::conditional< callableWith<F, T&&>::value, detail::argResult<false, F, T&&>, typename std::conditional< callableWith<F, T&>::value, detail::argResult<false, F, T&>, typename std::conditional< callableWith<F, Try<T>&&>::value, detail::argResult<true, F, Try<T>&&>, detail::argResult<true, F, Try<T>&>>::type>::type>::type>::type Arg; typedef isFuture<typename Arg::Result> ReturnsFuture; typedef Future<typename ReturnsFuture::Inner> Return; }; template<typename F, typename... Args> using resultOf = decltype(std::declval<F>()(std::declval<Args>()...)); |
這裡一大波template我們一個一個來。
1 |
FunctionReferenceToPointer |
可以無視掉,你可以想象成FF就是F,std::declval 讓你把 F變成 F&&,所以可以用
1 2 |
template<typename F, typename... Args> using resultOf = decltype(std::declval<F>()(std::declval<Args>()...)); |
這樣的語法拿到F(Args args…) 的返回值,不管F是object還是lambda。這樣,resultOf可以拿到我們的callback的返回type。現在我們得把這個返回值跟下一個函式的argument對應起來。這裡我們用callableWith
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
template<typename F, typename... Args> struct callableWith { template<typename T, typename = detail::resultOf<T, Args...>> static constexpr std::true_type check(std::nullptr_t) { return std::true_type{}; }; template<typename> static constexpr std::false_type check(...) { return std::false_type{}; }; typedef decltype(check<F>(nullptr)) type; static constexpr bool value = type::value; }; |
這裡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一個一個試過去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
template<typename T, typename F> struct callableResult { typedef typename std::conditional< callableWith<F>::value, detail::argResult<false, F>, typename std::conditional< callableWith<F, T&&>::value, detail::argResult<false, F, T&&>, typename std::conditional< callableWith<F, T&>::value, detail::argResult<false, F, T&>, typename std::conditional< callableWith<F, Try<T>&&>::value, detail::argResult<true, F, Try<T>&&>, detail::argResult<true, F, Try<T>&>>::type>::type>::type>::type Arg; typedef isFuture<typename Arg::Result> ReturnsFuture; typedef Future<typename ReturnsFuture::Inner> Return; }; |
我們就可以在編譯時間確保我們可以支援我們所有想支援的7個類啦。那具體拿著第一個callback的返回值怎麼傳輸到第二個callback上面做argument呢?這一段變種太多,我只給大家看最簡單的變種:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// Variant: returns a value // e.g. f.then([](Try<T>&& t){ return t.value(); }); template <class T> template <typename F, typename R, bool isTry, typename... Args> typename std::enable_if<!R::ReturnsFuture::value, typename R::Return>::type Future<T>::thenImplementation(F&& func, detail::argResult<isTry, F, Args...>) { static_assert(sizeof...(Args) <= 1, "Then must take zero/one argument"); typedef typename R::ReturnsFuture::Inner B; throwIfInvalid(); Promise<B> p; p.core_->setInterruptHandlerNoLock(core_->getInterruptHandler()); // grab the Future now before we lose our handle on the Promise auto f = p.getFuture(); f.core_->setExecutorNoLock(getExecutor()); // 這裡註釋省略,因為實在太長。 setCallback_([ funcm = std::forward<F>(func), pm = std::move(p) ]( Try<T> && t) mutable { if (!isTry && t.hasException()) { pm.setException(std::move(t.exception())); } else { pm.setWith([&]() { return funcm(t.template get<isTry, Args>()...); }); } }); return f; } template <class T> template <class F> void Future<T>::setCallback_(F&& func) { throwIfInvalid(); core_->setCallback(std::forward<F>(func)); } protected: typedef detail::Core<T>* corePtr; // shared core state object corePtr core_; |
core_ 是future 的member,也就是說我們設定的callback僅僅只是被加到callback裡面去了,還沒有被執行。只有當你執行future.get() 的時候值才會被你拿到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
template <class T> T Future<T>::get() { return std::move(wait().value()); } template <class T> Future<T>& Future<T>::wait() & { detail::waitImpl(*this); return *this; } template <class T> void waitImpl(Future<T>& f) { // short-circuit if there's nothing to do if (f.isReady()) return; FutureBatonType baton; f.setCallback_([&](const Try<T>& /* t */) { baton.post(); }); baton.wait(); assert(f.isReady()); } |
啊,所有的callback執行都被扔到baton裡面去了,我先不在這裡繼續深挖了。總之
1 |
auto value = folly::makeFuture(cb1).then(cb2).get(); |
- 在編譯的時候,是通過一系列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裡面比較有意思的是它儲存的機制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
using Storage = typename std::conditional<std::is_trivially_destructible<Value>::value, StorageTriviallyDestructible, StorageNonTriviallyDestructible>::type; Storage storage_; struct StorageTriviallyDestructible { // uninitialized uniocn { Value value; }; bool hasValue; StorageTriviallyDestructible() : hasValue{false} {} void clear() { hasValue = false; } }; struct StorageNonTriviallyDestructible { // uninitialized union { Value value; }; bool hasValue; StorageNonTriviallyDestructible() : hasValue{false} {} ~StorageNonTriviallyDestructible() { clear(); } void clear() { if (hasValue) { hasValue = false; value.~Value(); } } }; |
StorageTriviallyDestructible 還稍微合理一點,StorageNonTriviallyDestructible.clear() 裡面~Value() 絕對很少見。這是c++11裡面unconstrained union的新玩法,因為在unconstrained union裡面你必須有能力可以銷燬一個non-POD類,所以c++語法開放了~Value() 這種語法,讓你可以銷燬這個值。這個功能在這裡就被弄出了新玩法,被用來支援folly::Optional.clear(),這樣就算是一個NonTriviallyDestructible的物件你也可以隨時銷燬它。
這種unconstrained union的啟動機制也是比較麻煩的。folly::optional.set 長這樣:
1 2 3 4 5 6 7 |
template<class... Args> void construct(Args&&... args) { const void* ptr = &storage_.value; // for supporting const types new(const_cast<void*>(ptr)) Value(std::forward<Args>(args)...); storage_.hasValue = true; } |
1 |
new(const_cast<void*>(ptr)) Value(std::forward<Args>(args)...); |
這個叫placement new,就是說你給new 一個地址,new直接在你給的地址上面initialize,而不是去heap裡面佔記憶體。有上面兩個玩法的話,你就可以隨時隨地在c++啟動,銷燬這個值啦!
folly::Indestructible
如何確保你的meyer’s singleton永遠不死?這樣儲存你的類:
1 2 3 4 5 6 7 8 |
union Storage { T value; template <typename... Args> explicit constexpr Storage(Args&&... args) : value(std::forward<Args>(args)...) {} ~Storage() {} }; |
看起來好像沒有什麼特殊的對不對?不要忘記這個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 實在太麻煩。
先看數字轉字串裡面算多少位數的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
/** * Returns the number of digits in the base 10 representation of an * uint64_t. Useful for preallocating buffers and such. It's also used * internally, see below. Measurements suggest that defining a * separate overload for 32-bit integers is not worthwhile. */ inline uint32_t digits10(uint64_t v) { #ifdef __x86_64__ // For this arch we can get a little help from specialized CPU instructions // which can count leading zeroes; 64 minus that is appx. log (base 2). // Use that to approximate base-10 digits (log_10) and then adjust if needed. // 10^i, defined for i 0 through 19. // This is 20 * 8 == 160 bytes, which fits neatly into 5 cache lines // (assuming a cache line size of 64). static const uint64_t powersOf10[20] FOLLY_ALIGNED(64) = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000, 100000000000, 1000000000000, 10000000000000, 100000000000000, 1000000000000000, 10000000000000000, 100000000000000000, 1000000000000000000, 10000000000000000000UL, }; // "count leading zeroes" operation not valid; for 0; special case this. if UNLIKELY (!v) { return 1; } // bits is in the ballpark of log_2(v). const uint8_t leadingZeroes = __builtin_clzll(v); const auto bits = 63 - leadingZeroes; // approximate log_10(v) == log_10(2) * bits. // Integer magic below: 77/256 is appx. 0.3010 (log_10(2)). // The +1 is to make this the ceiling of the log_10 estimate. const uint32_t minLength = 1 + ((bits * 77) >> 8); // return that log_10 lower bound, plus adjust if input >= 10^(that bound) // in case there's a small error and we misjudged length. return minLength + (uint32_t) (UNLIKELY (v >= powersOf10[minLength])); #else uint32_t result = 1; for (;;) { if (LIKELY(v < 10)) return result; if (LIKELY(v < 100)) return result + 1; if (LIKELY(v < 1000)) return result + 2; if (LIKELY(v < 10000)) return result + 3; // Skip ahead by 4 orders of magnitude v /= 10000U; result += 4; } #endif } |
下面那個for loop多友好,上面x86那段是什麼鬼!
builtin_clzll 是gcc裡面的一個函式,具體定義看這裡Other Builtins 簡單來說就是算有幾個開頭的0的。這裡面的0是2進位制的0,所以63-leading zeros就是說二進位制裡面有幾位數。
1 |
const uint32_t minLength = 1 + ((bits * 77) >> 8); |
這個就是亮點了。高中數學沒學好的話,這裡強調一下
- log_10(v) =log_10(2) * log_2(v) (The Change-of-Base Formula) 。
- >> 8 就是除以以256
- bits在這裡已經是log(2)了
- +1是因為 0.3010略小於77/256我們繼續瞎,現在開始正式轉換
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/** * Copies the ASCII base 10 representation of v into buffer and * returns the number of bytes written. Does NOT append a \0. Assumes * the buffer points to digits10(v) bytes of valid memory. Note that * uint64 needs at most 20 bytes, uint32_t needs at most 10 bytes, * uint16_t needs at most 5 bytes, and so on. Measurements suggest * that defining a separate overload for 32-bit integers is not * worthwhile. * * This primitive is unsafe because it makes the size assumption and * because it does not add a terminating \0. */ inline uint32_t uint64ToBufferUnsafe(uint64_t v, char *const buffer) { auto const result = digits10(v); // WARNING: using size_t or pointer arithmetic for pos slows down // the loop below 20x. This is because several 32-bit ops can be // done in parallel, but only fewer 64-bit ones. uint32_t pos = result - 1; while (v >= 10) { // Keep these together so a peephole optimization "sees" them and // computes them in one shot. auto const q = v / 10; auto const r = static_cast<uint32_t>(v % 10); buffer[pos--] = '0' + r; v = q; } // Last digit is trivial to handle buffer[pos] = static_cast<uint32_t>(v) + '0'; return result; } |
這裡面開始甩說用pos慢了,估計原因是loop unrolling做不了,但是具體不好說,我得問問他。
peephole optimization簡單來說就是一段短小精悍的程式碼可以被compiler 變得更短小精悍(Peephole optimization),具體肯定也是實測過才敢拿出來講。非常好讀懂我就不BB了。不過用C++用的熟練的看到這種不差buffer大小的肯定非常不爽。為什麼可以不查?!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * int32_t and int64_t to string (by appending) go through here. The * result is APPENDED to a preexisting string passed as the second * parameter. This should be efficient with fbstring because fbstring * incurs no dynamic allocation below 23 bytes and no number has more * than 22 bytes in its textual representation (20 for digits, one for * sign, one for the terminating 0). */ template <class Tgt, class Src> typename std::enable_if< std::is_integral<Src>::value && std::is_signed<Src>::value && IsSomeString<Tgt>::value && sizeof(Src) >= 4>::type toAppend(Src value, Tgt * result) { char buffer[20]; if (value < 0) { result->push_back('-'); result->append(buffer, uint64ToBufferUnsafe(-uint64_t(value), buffer)); } else { result->append(buffer, uint64ToBufferUnsafe(value, buffer)); } } |
好了我服了。。。
———————–原先的答案——————————
AtomicStruct
folly/AtomicStruct.h at master · facebook/folly · GitHub
類似於std::atomic, 但是任何小於8個byte的POD類都可以變成atomic的。實現的方法如下:
用一個unconstrained union 來存資料:
1 2 3 4 |
union { Atom<Raw> data; T typedData; }; |
T是你的類,Atom 就是std::atomic,Raw是這麼來的
1 |
typename Raw = typename detail::AtomicStructIntPick<sizeof(T)>::type |
1 2 3 4 5 6 7 8 |
template <> struct AtomicStructIntPick<1> { typedef uint8_t type; }; template <> struct AtomicStructIntPick<2> { typedef uint16_t type; }; template <> struct AtomicStructIntPick<3> { typedef uint32_t type; }; template <> struct AtomicStructIntPick<4> { typedef uint32_t type; }; template <> struct AtomicStructIntPick<5> { typedef uint64_t type; }; template <> struct AtomicStructIntPick<6> { typedef uint64_t type; }; template <> struct AtomicStructIntPick<7> { typedef uint64_t type; }; template <> struct AtomicStructIntPick<8> { typedef uint64_t type; }; |
我看到這裡已經開始瞎了。compare exchange是這樣的
1 2 3 4 5 6 7 8 9 10 11 |
bool compare_exchange_weak( T& v0, T v1, std::memory_order mo = std::memory_order_seq_cst) noexcept { Raw d0 = encode(v0); bool rv = data.compare_exchange_weak(d0, encode(v1), mo); if (!rv) { v0 = decode(d0); } return rv; } |
裡面的encode/decode就是拿來騙編譯器的memcpy。寫了這麼多廢話,說白了就是為了讓編譯器開心的可以用各種std::atomc<int>DiscriminatedPtr
用法就是boost::variant,但是用DiscriminatedPtr沒有任何多餘的代價,就是一個指標的大小。為什麼可以沒有代價呢?應為64位系統裡面其實只有48位拿來做地址了,剩下16位是沒有被系統用起來的。所以要地址是這麼讀的
1 2 3 |
void* ptr() const { return reinterpret_cast<void*>(data_ & ((1ULL << 48) - 1)); } |
那前16個bit是存什麼呢?存的是現有這個類的index。每次存的時候,會通過index找到對應的類
1 2 3 4 5 6 7 8 |
/** * Set this DiscriminatedPtr to point to an object of type T. * Fails at compile time if T is not a valid type (listed in Types) */ template <typename T> void set(T* ptr) { set(ptr, typeIndex<T>()); } |
然後
1 2 3 4 5 6 |
void set(void* p, uint16_t v) { uintptr_t ip = reinterpret_cast<uintptr_t>(p); CHECK(!(ip >> 48)); ip |= static_cast<uintptr_t>(v) << 48; data_ = ip; } |
那typeIndex是什麼鬼!?typeIndex是一個編譯是通過遞迴製造出來的列表,可以在編譯時製造出一個數字對應類的列表
1 2 3 4 5 6 7 8 9 10 11 12 |
template <typename... Types> struct GetTypeIndex; template <typename T, typename... Types> struct GetTypeIndex<T, T, Types...> { static const size_t value = 1; }; template <typename T, typename U, typename... Types> struct GetTypeIndex<T, U, Types...> { static const size_t value = 1 + GetTypeIndex<T, Types...>::value; }; |
具體實現在這裡 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•
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#define _ F-->00||-F-OO--; int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO() { _-_-_-_ _-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_-_-_-_-_ _-_-_-_-_-_-_-_ _-_-_-_ } |
注:這段程式實際上是 1989 年修正過的,由於 88 年原來程式程式碼沒有考慮到 ANSI C 的編譯標準,導致在處理例如
1 2 |
#define _ -i -_ |
的時候,老舊的 K&R 框架和 ANSI C 結果不一樣:K&R 是直接的
1 |
--i |
而 ANSI C 編譯結果實際上等同於
1 |
-(-i) |
因此之前的程式現在執行的話出來的結果是 0.250,而不是 3.141。修正過的程式就沒有這個問題了。
又比如 13 年有個只有一行的程式,可以判斷從 Franklin Pierce 往後的 31 位美國總統是民主黨還是共和黨,這個就有點不知所云了:
•cable1.c•
1 |
main(int riguing,char**acters){puts(1[acters-~!(*(int*)1[acters]%4796%275%riguing)]);} |
使用方法:
1 2 3 4 |
make cable1 ./cable1 obama republican democrat ./cable1 bush republican democrat |
總統名要小寫,republican 和 democrat 順序不能顛倒。
經 @chua zier 提醒,歷史上的確有重名的美國總統,除了 Johnson 之外,還有 Theodore Roosevelt / Franklin D. Roosevelt,程式原作者註明用
1 |
./cable1 roosevelt republican democrat |
表示 Theodore Roosevelt,而用
1 |
./cable1 fdr republican democrat |
表示 Franklin D. Roosevelt。
這一行程式碼做了這麼多事:首先查詢輸入的總統的名字,然後在一個 look-up table 裡面找出對應的政治陣營,再輸出出來。問題在於這 31 位總統名字存放在哪裡?而這個 look-up table 又存放在哪裡?
有趣的是,IOCCC 的評委還提到,你甚至可以用這個程式檢測一些 IT 大佬的 Mac / PC 陣營:
1 2 3 4 5 6 |
./cable1 Cooper Mac PC ./cable1 Noll Mac PC ./cable1 Broukhis Mac PC ./cable1 Jobs Mac PC ./cable1 Gates Mac PC ./cable1 Ballmer Mac PC |
難道這個程式暴露了 Ballmer 離開微軟的真相?
最近幾屆比賽的程式碼為了增加混亂程度,程式碼越來越長,可讀性也越來越差(不過話說回來,讓可讀性變得越來越差其實原本就是這個比賽的第一宗旨吧),不少程式碼甚至本身就是個 ASCII artwork……比如 11 年有一隻阿卡林:
•akari.c•
為了保持美觀我就直接上圖了。原始碼見此:http://www.ioccc.org/2011/akari/akari.c
–––––––––– !!! 前方阿卡林軍團高能預警 !!! ––––––––––
這個阿卡林程式實際上是一個影像 down-sampler,可以接受符合條件的 PGM / PPM 灰度影像或者 LF 換行(不支援 CR-LF)的 ASCII art 為輸入,然後轉換輸出一個處理後的影像 / ASCII art。不過這個阿卡林最逆天的地方在於,它可以用自身的原始碼文字作為輸入,輸出生成另一個可以編譯執行的程式的程式碼!而且把這個生成的程式文字繼續作為輸入做進一步 down-sample,又可以生成一段可以編譯的程式碼,如此反覆,可以套多達4層!詳細的食用方法如下:
1 2 |
make akari ./akari akari.c akari2.c |
然後生成的阿卡林·2號是這個樣子的:
•akari2.c•
看不清?請摘下眼鏡或者退遠了看。注意,阿卡林·2號也是可以編譯執行的,她的功能是把輸入的 ACSII 文字的每個字元中間插入空格以及每行之間插入空行,生成一段“疏鬆”了的文字。我們用阿卡林·2號自己做實驗品:
1 2 |
make akari2.c ./akari2 <akari2.c> akari2fat.txt |
成功了!生成了一隻阿卡林·2號·舒鬆:
•akari2fat.txt•
阿卡林·2號還能幹別的,她支援一個 rot13 引數:
1 |
./akari2 rot13 <akari2.c> akari2fat.txt |
生成的是經過 ROT13 仿射變換的文字,我們稱之為阿卡林·2號·舒鬆·加蜜吧!
但是還沒完……如果我們把原版阿卡林放進去再來一層呢?
1 |
./akari < akari.c | ./akari > akari3.c |
於是阿卡林·3號誕生:
•akari3.c•
1 2 3 4 5 6 7 8 9 |
wm_aoi(n) /*ity,,[2*/{}char*y= (")M{lpduKtjsa(v""YY" "*yuruyuri") ;main(/* /",U/ R)U* Y0U= ="/\ */){puts (y+ 17/* "NR{I=" ){/=* =* */);/* **/{ ;;}} |
可憐的阿卡林·3號,由於“馬賽克”(down-sample)次數太多,摘了眼鏡也只能模糊看到一點點……我們來問問阿卡林·3號對於誕生的感受吧:
1 2 |
make akari3 ./akari3 |
於是她回答:
1 |
yuruyuri |
居然會說ゆるゆり!
最後,我們嘗試生產一下阿卡林·4號:
1 |
./akari < akari.c | ./akari | ./akari > akari4.c |
•akari4.c•
1 2 3 4 5 |
main (){puts("Y" "U RU YU "\ "RI" )/* */ ;} |
順利生產!雖然內容已經直截了當了,不過我們還是採訪一下她吧:
1 2 |
make akari4 ./akari4 |
她的答覆是:
1 |
YU RU YU RI |
至此,阿卡林軍團全部誕生!
不得不佩服作者構建程式碼的精妙程度。他的個人主頁在這裡:uguu… (這位作者其實已經是這比賽的常客了,先後一共拿過 6 次不同的獎項。)
經 @馬琦明 提醒,我又把上面這位作者的另一個作品搬出來了,13 年的 Most Catty——炮姐程式。這程式的程式碼長這個樣子:
•misaka.c•
原始碼:http://www.ioccc.org/2013/misaka/misaka.c
對的,當你看到原來是這個“御阪”的時候,你就知道,我們要開始造(kè)人(lóng)了……
1 |
make misaka |
這個御阪的作用是把輸入的 ASCII 橫向連線起來。首先連線兩個自己試試:
1 |
./misaka misaka.c misaka.c > misaka2.c |
“把兩個御阪輸入一個御阪,會生成什麼?”“兩個御阪。”
•misaka2.c•
聽起來很不可思議但是在這位作者的構建下完全不出意外地,上面這個御阪-2 居然也是可以編譯執行的:
1 |
make misaka2.c |
御阪-2 的功能是把輸入的 ASCII 縱向連線起來。那我們就試著縱向連線兩個御阪:
1 |
./misaka2 misaka.c misaka.c > misaka3.c |
於是御阪-3 誕生了:
•misaka3.c•
我們來執行一下這個御阪-3。你此時腦中的景象可能是這樣的:
但是你錯了,御阪-3 會給你造出來更加精神汙染的那隻 long cat:
1 2 |
make misaka3 ./misaka3 |
沒錯就是這隻喵:
這裡其實有 Unix 的 cat 指令的梗……如果之前你在執行御阪-2 的時候,用了更多的御阪作為輸入,例如 4 個:
1 2 3 |
./misaka2 misaka.c misaka.c misaka.c misaka.c > misaka4.c make misaka4.c ./misaka4 |
那麼御阪-4 會給你造一隻更長的 looooong cat:
按作者的意思,你可以最多疊加 31 個御阪來生成一隻 looo….ooong cat(具體上限由編譯器的 sizeof(int) 決定)。
13 年還有浙大教授侯啟明寫的 ray tracer 程式,雖然程式碼本身存在爭議是否符合比賽規則,例如為避免長度超限制而使用了一些壓縮方法、程式是個死迴圈。如果這段程式可讀性不是這麼噁心的話其實還是非常值得鑽研的,裡面用到了很多有趣的資料結構和著色體系。
食用方法也很簡單,把程式掛在那兒跑一晚上,強制退出,就可以看結果了。由於是無窮盡的遞迴,程式跑的時間越長,影像就越精緻。詳細的說明和原始檔還是參考官網咖:
Previous IOCCC Winners with spoilers
這裡有個示例圖。
侯老師還有另外三個作品上榜,一個是極其酷炫的 syntax highlightener,還有一個(原始碼本身就是 GUI 的)科學計算器,後面這個已經有人 @paintsnow 回答過了。最新一個是上個月剛剛公佈的新一屆獲獎作品 MD5 without integers,但是這個的原始碼還沒有公佈,估計要等到明年了。
三、平方根倒數速演算法
伯小樂看到有其他網友提到了平方根倒數速演算法
1 2 3 4 5 6 7 8 |
float FastInvSqrt(float x) { float xhalf = 0.5f * x; int i = *(int*)&x; // evil floating point bit level hacking i = 0x5f3759df - (i >> 1); // what the fuck? x = *(float*)&i; x = x*(1.5f-(xhalf*x*x)); return x; } |
這個技巧就不詳細解釋了,推薦大家重新再看看這篇文章《數學之美:平方根倒數速演算法中的神奇數字 0x5f3759df 》。