發表文章

目前顯示的是 八月, 2013的文章

C++ 的 C style cast 不等於 static_cast

即使看了幾次《Effective C++》item 27 "Minimize casting", 還是不明白 C style cast 有什麼重大問題。只覺得新的 cast 比較明確安全, 但寫起來很囉唆, 又是角括號又是括號 (愛惜手指, 請從少按 shift 做起)。一直以為 C style cast 等同於 static_cast, 這樣的話, 在使用 static_cast 的場合, 就偷懶寫成 C style cast 吧。 今天看到 Ken 寫的 On C-Style Cast in C++, 才發覺情況和我想得不同。於是寫個小程式驗證一下:$ cat b.cpp -n 1 class A {}; 2 class B : public A {}; 3 class C {}; 4 5 int main(void) { 6 A* a = new A(); 7 B* b = (B*)a; 8 C* c = (C*)a; 9 B* b2 = static_cast<B*>(a); 10 C* c2 = static_cast<C*>(a); 11 return 0; 12 } $ g++ b.cpp -o b b.cpp: In function ‘int main()’: b.cpp:10:28: error: invalid static_cast from type ‘A*’ to type ‘C*’ 果真 C style cast 可以隨意轉換, static_cast 限制較為嚴格一些, 但是 pointer-to-base 仍可透過 static_cast 轉成 pointer-to-derived (即使轉型後會有問題)。若需要更安全的作法, 可以用 dynamic_cast, 看轉完的值是否為空指標, 可以知道是否能安全轉過去。代價是會增加執行時間, 詳細說明見《Effective C++》item 27 "Minimize casting"。關於轉型最重要的觀念, 大概就是盡量別轉型吧。

C++ 指標轉型後位置可能會改變

今天踩到一個和 C++ 指標轉型相關的 bug, 幸好以前看《深度探索 C++ 物件模型》時有個模糊的印象, 知道物件指標轉型後, 位置可能會變, 發現這問題後很快就想通了。 先寫個小程式實驗一下:#include <iostream> struct A { int x; }; struct B { int y; }; struct C : public A, B { int z; }; int main() { C *c = new C(); A *a = c; B *b = c; void* v = c; std::cout << a << " " << b << " " << c << " " << v << std::endl; return 0; } 範例輸出:0xbd0010 0xbd0014 0xbd0010 0xbd0010 由此可得知 a、c、v 位置一樣, 但 b 不同。 若用指標相等做邏輯判斷, 要留意轉型帶來的影響, 特別是中間有轉型成 void* 的時候, 可能傳了同樣的物件, 卻因型別不同造成誤判。 以下是一個示意的例子:class Foo final { public: void Save(const Data& data, void* source); ... private: ... FooCallback *m_callback; } void Foo::Save(const Data& data, void* source) { ... if (m_callback && source != m_callback) { m_callback->OnSave(data); } } 有不同的來源會呼叫 Foo::Save, 此時 Foo 會通知事先註冊的 callback, 但要避開 callback object 主動呼叫 Foo::Save 的情況。 乍看之下有檢查 source != m_callback 即可安心。但若 callback o…

以安裝 ccache 為例, 說明如何使用系統工具除錯

ccache 是什麼?ccache 藉由暫存編譯過的 object 檔, 可以減少不必要的重新編譯時間。 用法很簡單:# 安裝 $ sudo aptitude install ccache # 啟用 ccache $ export PATH="/usr/lib/ccache:$PATH" $ gcc ... # 這時會用到 /usr/lib/ccache/gcc 以我測試的例子來說, 從頭重新編譯一次是半小時左右, 裝了 ccache 後變成一分半。 依 ccache 官網所言, ccache 有可能重編譯不必要的程式, 但不會用到不對的暫存檔, 這也是使用這類工具時最重要的保證。 不過這篇的重點不在 ccache 的用法, 而是如何「知道如何安裝和使用它」。以前的文章或多或少有提到下文用的工具, 這篇比較有系統地用一個完整的例子使用它們。 已知 ccache 替換 gcc 等編譯工具, 檢查編譯條件和檔案沒變時, 直接取得暫存檔作為編譯結果。 粗略地掃過 ccache manual, 得知是用 symbolic link 替換 gcc 為 ccache。 截至目前為止, 我們已得知 ccache 的核心運作原理, 接下來可以動手試看看。 安裝和使用過程1. 首先確認是否能透過系統套件安裝 ccache:$ aptitude search ccache i ccache - Compiler cache for fast recompilation of C/C++ code p ccache:i386 - Compiler cache for fast recompilation of C/C++ code 2. 再來看套件訊息, 確定沒搞錯套件, 還有看看版號是否夠新: $ aptitude show ccache Package: ccache State: installed Automatically installed: n…

用 gdb 略過部份程式碼

尋找「誰是兇手」的時候, 以往的作法是註解掉部份程式、重新編譯、重新執行、觀察結果是否有變化。然後反覆前述動作直到找到兇手為止。編譯的時間略長的時候, 這個過程頗為痛苦。 幸好 gdb 有跳掉部份程式碼的方法。其中一個是用 return 結束目前所在的函式, 返回上一層 frame。另一個作法是先設中斷點, 再用 jump 跳到中斷點。 用 return 的小小缺點是: gdb 每次都會問你 yes/no, 用 jump 則無此困擾。可以用 define 寫個簡單的函式, 設暫存中斷點, 再用 jump 跳到中斷點, 藉此忽略中間的程式碼。 作法如下: # skip the next N line statements define j if $argc == 1 && $arg0 > 0 tbreak +$arg0 jump +$arg0 end end 之後就可以用 j N 跳掉接下來的 N 行程式。

有繼承的情況下, C++ method 存取到誰的 member field?

Effective C++ item 27 "盡量少做轉型" 提到下列轉型看起來像對的, 實際是錯的:class SpecialWindow : public Window { public: virtual void onReSize() { static_cast<Window>(*this).onResize(); ... } } 對這點感到很納悶, 於是寫個小程式實驗看看。結果發覺自己沒看清楚, 書上舉例是轉型 Window, 不是轉型 Window*, 所以 Window::onResize() 作用到新產生的物件身上, 結果不同於呼叫 Window::onResize()。 既然已經寫了小程式做實驗, 順便記在這裡供日後備忘。 要點如下: 類別 C 的 method 會存取自己的 member field x, 若 C 本身沒有這個 member field, 會往父類別 A 找。 承上, A 和 C 的 method 存取到的 x 是同一個 x, 也就是 A::x。 若類別 B 宣告和父類別 A 同名稱的 member field x, 則類別 B 和父類別 A 各自擁有一份不同位置的 member field x。 承上, A 的 method 會存取 A::x; B 的 method 存取 B::x。呼叫到誰的 method, 就知道改到誰的 x。 題外話, 允許子類別覆寫或重覆宣告同名 method 或 member field 是個糟糕的主意, 再加上可以轉型, 而且轉型後結果還會不一樣, 真是火上加油啊.......。了解 C++ 愈多, 愈覺得要從實作層面才能理解它的語法。 程式碼範例輸出$ ./a b.set_x(3) 3 0 copy constructor A is called 3 0 3 30 3 30 c.set_x(3) 3 3 3 3 3 30 3 30 method address &A::set_x 0x4006c8 &B::set_x 0x400710 &C::set_x 0x4007da member field address &b.x 0x7fffb4ef43a4 &a…

C++: 使用 private virtual 區分實作介面的 method

問題描述Java 有語法 interface 明確定義 class 之間的接口, 但是 C++ 沒有, 只能透過「習以為常」的慣例表示, 也就是: class I 宣告一組 public pure virtual function, 表示 I 是一個 "interface" class A 希望實作 I, 於是透過繼承的方式實作 I 需要用 I* 的 class, 取得實作 I 的物件 (也就是 A 的物件), 存成 I* 當 class A 需要實作多組介面, 或是自己也有一些 public method 供別人使用時, 看 class A 的宣告會不易找出那些是 A 的 public API, 比方說以下的例子: class I { public: virtual void foo() = 0; virtual void bar() = 0; }; class A : public I { public: void handle(); virtual void process(); // I's methods. virtual void foo(); virtual void bar(); }; 一眼看去有四個 method, 實際上可能只有 handle() 和 process() 才是使用 A 的人需要關心的。當 A 同時實作 (繼承) 多組介面就不易閱讀了。 解法若宣告實作介面的 method 為 private, 像下面這樣: class A : public I { public: void handle(); virtual void process(); private: // I's methods. virtual void foo(); virtual void bar(); }; 有幾點好處: 降低這些 method 可存取的範圍, 避免被誤用 縮小閱讀程式的範圍 間接暗示它們是作為 I 的介面使用, 若 A 自己的程式碼都沒用到它們的話, 會更明確地表明此點。特別適合用於 callback 的介面。宣告 private virtual 的基本精神和宣告 member field …

用 doxygen 產生 class hierarchy diagram

最近需要讀比較複雜 C++ 程式, 常常看到一堆 class 有個祖宗十八代, 要找出眾多子代的 class 或是某個 method 到底是那一代祖先實作的, 有些麻煩。 原本想自己玩看看 clang, 用 clang 分析原始碼產生 graph, 再套 graphviz 畫出來。轉念一想, 這麼常見的需求, 應該有人做好了。況且, 自己處理單檔可能不會麻煩, 但若多個檔案需要不同 include path (-I) 時, 到也有些頭痛。 上網查一下, 發覺 stackoverflow 有人推薦用 doxygen, 一試就靈, 最棒的是 doxygen 不需另外讀編譯 C/C++ 的設定檔, 它直接讀取指定的目錄。先跑 doxygen -g myconfig 產生設定檔 myconfig, 再修改 myconfig: INPUT RECURSIVE = YES 最後執行 doxygen myconfig 即可。 產生的網頁裡可看到各個 class 的祖譜, 非常方便。我猜 doxygen 可能是直接做文字分析, 不是用 compiler 下手。 實測的結果也的確有問題, Ubuntu 12.04 的 doxygen 有點舊, class 有用到 C++11 的 keyword final 時, final 反而會被當成 class name。幸好在 doxygen 1.8.2 版已解決, 先裝 doxygen 舊版, 再自己抓原始碼編譯, 用 checkinstall 封裝成 package 即可解掉這問題。 用 checkinstall 封裝的好處是, 可從 dpkg -L PKG_NAME 看裝的內容, 日後有問題也可用 dpkg -r PKG_NAME 刪除。Btw, doxygen 參數一堆, 有閒再來研究看看有什麼好用的東西。2013-09-10 更新若遇到 macro 造成 doxygen 分析錯誤, 可試著要求 doxygen 展開特定的 macro。作法是修改下列的設定:ENABLE_PREPROCESSING = YESMACRO_EXPANSION = YESEXPAND_ONLY_PREDEF = YESPREDEFINED = ABC=xyzEXPAND_AS_DEFINED …

用 python gdb 客製化 backtrace 的結果 (2)

之前寫的指令 bt 加上參數 -s, 使用 -s 時, 會一併記錄 backtrace 每個 frame 附近的原始碼。 以下是一個範例輸出: (gdb) bt -s #0 A::hello at b.cpp:8 | class A | { | public: | void hello(int n) | { ->| std::cout << n << std::endl; | } | | void foo(int n) | { #1 A::bar at b.cpp:18 | bar(n + 1); | } | | void bar(int n) | { ->| hello(n + 10); | } | }; | | int main(void) { #2 A::foo at b.cpp:13 | std::cout << n << std::endl; | } | | void foo(int n) | { ->| bar(n + 1); | } | | void bar(int n) | { #3 main at b.cpp:24 | } | }; | | int main(void) { | A a; ->| a.foo(5); | return 0; | } 修改後的 python script 如下: 作法大致和 《用 python gdb 客製化 backtrace 的結果》一樣, 只是改取檔案的 fullname, 再自己讀出相關的原始碼。這部份用 python 實作, 相當地輕鬆寫意。

用 macro 組合變動式參數, 強化 log 函式

程式除錯時常需要加些 log 印出變數內容, 通常會希望附帶印出所在的函式。每次要自己重打一次函式名稱太麻煩了, 可以利用 C 的 macro 避免重覆的動作。 先來看個範例程式: 以及輸出結果:$ g++ a.cpp -o a; ./a result: 3 $ g++ -DDEBUG_PRINT a.cpp -o a; ./a int Calculator::add(int, int) this=0x7ffff03c1d7f a = 2, b = 1 result: 3 有 #define DEBUG_PRINT 的才會真的執行 DebugPrintf, 除了兼顧原本 printf 有的變動式參數外, 順便自動補上函式名稱和目前物件的指標; 反之則完全不會執行到, 不會增加額外負擔。 有需要的話, 可以稍微修改 DebugPrintf, 如同 《trace C/C++ function call 的方法》 記錄檔名和行數。 關於巨集這行:#define DebugPrintf(format, args...) fprintf(stderr, "%s this=%p " format, __PRETTY_FUNCTION__, this, ##args) 用到的語法包含: C 的 "abc" "def" 會組成 "abcdef" args... 比 __VA_ARGS__ 易於閱讀 只有一個參數的時候, __VA_ARGS__ 會自動移掉前一個 "," 可以參照 Variadic Macros - The C Preprocessor 了解細節。

用 python gdb 客製化 backtrace 的結果

需求想要了解模組之間函式呼叫的關係時, 與其一層層比對多個類別之間的呼叫關係, 不如直接在最後一個呼叫函式放中斷點, 直接顯示 backtrace。但是當函式裡有太多參數或 template 時, backtrace 的 frame 訊息會變得很長, 不易閱讀。我的目的只是找出呼叫的函式名稱、檔名和行數, 函式帶的參數反而是困擾。 作法一: 用 gdb.execute() 一個簡單的作法是截取 gdb 的輸出, 然後解析文字去掉不要的部份: Btw, 上面的作法還順便幫行首的標號上色。 但是, 使用 cgdb 時會無法運作, 理由是 cgdb 使用 GDB MI, gdb.execute('backtrace') 的結果不是原本看到的格式, 難以解析。 作法二: 用 gdb.Frame() API 只好改用中規中矩的方式逐一讀取 frame, 取出需要的資訊: 將上面的 script 存到 /path/to/gdb/scripts/backtrace.py, 接著在 $HOME/.gdbinit 裡加入以下設定:python sys.path.insert(0, '/path/to/gdb/scripts') import backtrace end 之後就能用 bt 顯示精簡後的 backtrace 了, 也方便手動複製貼上到筆記裡。以下是一個輸出例子: (gdb) bt # 0 A::hello at a.cpp:8 # 1 A::bar at a.cpp:13 # 2 A::foo at a.cpp:18 # 3 main at a.cpp:25 Btw, 若是需求比較簡單, 可以試看看 Print Settings, 有些選項可以改變 backtrace 顯示的訊息。 參考資料:The Cliffs of Inanity › 2. Writing a new gdb commandPruning backtrace output with gdb script - Stack OverflowFrames In Python - Debugging with GDBSymbols In Python - Debugging with GDBSymbol Tables In Python - Debuggi…