2013年8月28日 星期三

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"。關於轉型最重要的觀念, 大概就是盡量別轉型吧。

2013年8月27日 星期二

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 object (即有繼承 FooCallback 的類別的物件) 呼叫 Foo::Save 時這麼寫:

Foo *foo = new Foo();
...
foo->Save(data, this);

有可能因指標型別不同, 而得到不同的位置, 導致 source != m_callback 為真。

反之, 這樣的寫法才能安全過關:

foo->Save(data, (FooCallback*)this);

C++ 的世界真是處處驚奇啊!

2013年8月25日 星期日

以安裝 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: no
Version: 3.1.6-1
...
Description: Compiler cache for fast recompilation of C/C++ code
 ccache is a compiler cache. It speeds up recompilation by caching previous compilations and detecting when the same compilation is being done again. Supported languages are C, C++,
 Objective-C and Objective-C++.
Homepage: http://ccache.samba.org

對照官網版號 3.1.9, 3.1.6-1 還算 OK, 日後有必要再仔細看 release note 決定是否要抓原始檔重編。

3. 開始安裝:

$ sudo aptitude ccache

裝完後沒任何反應 ......, 依先前得知的訊息, 看看 gcc 有沒有被換掉:

$ which gcc
/usr/bin/gcc
$ ls -l /usr/bin/gcc
lrwxrwxrwx 1 root root 7 Nov 14  2012 /usr/bin/gcc -> gcc-4.6*
$ ls -l /usr/bin/gcc-4.6
-rwxr-xr-x 1 root root 353216 Apr 16  2012 /usr/bin/gcc-4.6*

結果 gcc 沒有被換掉 ......。

4. 腦袋卡住了一下下, 接著想到來確認 ccache 裝了什麼, 也許有些線索:

$ dpkg -L ccache
/.
/usr
/usr/share
...
/usr/share/man/man8/update-ccache-symlinks.8.gz
...
/usr/sbin/update-ccache-symlinks
/usr/bin
/usr/bin/ccache
/usr/lib
/usr/lib/ccache

裡面有個可疑的檔案 /usr/sbin/update-ccache-symlinks, 馬上來看看檔案內容。

結果是個 perl script, 從開頭得知重要的資訊:

  6 my $ccache_dir = "/usr/lib/ccache";
  7 my $old_gcc_dir = "/usr/lib/gcc";
  8 my $new_gcc_dir = "/usr/lib/x86_64-linux-gnu/gcc";
  9 my %old_symlinks; # Current compiler names in /usr/lib/ccache
 10 my %new_symlinks; # Compiler names that should be in /usr/lib/ccache
 11 my @standard_names = qw(cc c++);

對照後面的程式碼和這些變數名稱, /usr/lib/ccache 是關鍵的角色:

$ ls -l /usr/lib/ccache/
total 0
...
lrwxrwxrwx 1 root root 16 Aug 24 21:19 g++ -> ../../bin/ccache*
lrwxrwxrwx 1 root root 16 Aug 24 21:19 g++-4.6 -> ../../bin/ccache*
lrwxrwxrwx 1 root root 16 Aug 24 21:19 gcc -> ../../bin/ccache*
lrwxrwxrwx 1 root root 16 Aug 24 21:19 gcc-4.6 -> ../../bin/ccache*
...

Bingo!! 將 /usr/lib/ccache 加到 PATH 前面, 應該會有效果。但是, 要如何快速驗證 ccache 確實有發揮效果呢? 我可不想重編一堆檔案, 過個半小時才發覺「好像沒效」。

依前面的假設, ccache 編譯時會從暫存區取出編譯過的檔案, 所以第一次編譯時, ccache 應該也會寫入編譯過的檔案到暫存區。幸好開檔的 system call 只有一個 open(2), 這時就該 strace 上場了:

$ cd ~/tmp/
$ export  PATH="/usr/lib/ccache:$PATH"
$ strace -eopen -f g++ t.cpp -c > log 2>&1

翻一下 log 會看到

 10 [pid 18133] open("/home/fcamel/.ccache/tmp/t.tmp.fc-vm.18132.ii", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC, 0666) = 4
 11 [pid 18133] open("/home/fcamel/.ccache/tmp/tmp.cpp_stderr.fc-vm.18132", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC, 0666) = 4

由此順便找到暫存目錄在 $HOME/.ccache 下。日後有需要也可以到這裡清清暫存檔。

有興趣的話可以用 strace 追更多 system call, 比對 open 開啟的 file descriptor (fd) 和後續 read、write 操作的 fd, 可以證實更多推測。到此就大功告成了!

小結

相信以 ccache 這樣成熟的套件, 仔細翻翻官網文件、man page 或 google 一下, 就可以找到答案。不過了解如何使用這些系統工具以後, 日後遇到文件不詳細或不易 google 的套件時, 有機會自行找出一線生機。

2013年8月24日 星期六

用 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 行程式。

2013年8月22日 星期四

有繼承的情況下, 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*)&b)->x  0x7fffb4ef43a0
&c.x          0x7fffb4ef43b0
&((A*)&c)->x  0x7fffb4ef43b0

2013年8月21日 星期三

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 為 private 差不多: 不需要用到的, 就不要曝露出去, 易於日後維護。

相關閱讀

本篇討論的使用情境很窄, 專注於一點: 需要在 C++ 的世界裡表示出「Java interface」的語意時, 可使用「在介面 class 宣告 public pure virtual method + 在實作 class 宣告 private virtual method」。

Virtuality 提出關於 virtual 的四項要點:

  • Prefer to make interfaces nonvirtual, using Template Method.
  • Prefer to make virtual functions private.
  • Only if derived classes need to invoke the base implementation of a virtual function, make the virtual function protected.
  • A base class destructor should be either public and virtual, or protected and nonvirtual.

涵蓋關於 private virtual 的更多應用情境, 滿不錯的參考資料。

2013年8月19日 星期一

用 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 = YES
  • MACRO_EXPANSION = YES
  • EXPAND_ONLY_PREDEF = YES
  • PREDEFINED = ABC=xyz
  • EXPAND_AS_DEFINED = ABC

其中 EXPAND_AS_DEFINED 填入希望展開的 macro。若執行後沒有展開成功, 可以設 PREDEFINED 告知 doxygen 如何展開目標 macro。見 Doxygen Manual: Preprocessing 了解和其它前處理相關的設定。

2013年8月15日 星期四

用 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 實作, 相當地輕鬆寫意。

2013年8月10日 星期六

用 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 了解細節。

2013年8月3日 星期六

用 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 顯示的訊息。

參考資料:

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...