2013年9月26日 星期四

對 C++ 的觀感

寫了快兩年的 C++, 覺得 C++ 是學習成本高昂, 投資報酬率卻很低的語言。剛好看到 Less is exponentially more 有感, 在 G+ 寫了一些感想。提及經年累月的 C++ 經驗也會變為機會成本, 阻礙程式設計師轉向其它語言。不過在 Go 有龐大的 C++ library 和產品替代品以前, 也不容易選定用 Go 開發有效率需求的程式。實在是兩難啊。

下面取 G+ 留言的其中一段說明為什麼我覺得學 C++ 投資報酬率很低。

舉一個大家可能都會遇到的例子: 執行時多型。

在 Java 和 Python 裡, 說明執行時多型只要一句話就結束了, 就.....看那物件指到誰, 就會執行它的 method。

C++ 的話, 要明白兩件事:

  • 是否有使用指標呼叫? 等等, 還有參考的效果等同指標, 也要留意是否有用參考 ( 雖然很「理所當然」, 多了一個細節要留意 )
  • 呼叫的 method 是否有宣告 virtual? 如果你在目標類別的 method 沒看到 virtual, 記得還要順便檢查它的全部父類別是否有宣告該 method 為 virtual, 因為只要父類別有宣告 virtual, 子類別可以省略 virtual 的宣告

再來要明白 pure virtual 以及漏實作 virtual 函式造成的不易理解的連結錯誤訊息。到目前為止還只是基本語法, 還沒到實戰注意事項。

實戰注意事項包含:

  • 若類別有可能被繼承, 記得宣告 destructor 為 virtual
  • private virtual 的使用時機
  • 不能在 constructor 和 destructor 裡呼叫 virtual method

可能還有其它點, 目前有需要學到的就這幾點。明白為什麼後會覺得很「直覺」, 但是每當有必要用到時, 就得分神查一下這是在做什麼或是為什麼這樣可以運作。我明白 virtual 的出發點是 C++ 的核心精神 zero cost, 只是在看到這麼多衍生出來瑣碎的注意事項後, 我滿懷疑執行期的 zero cost 是否 >> 開發者的時間成本, 特別是獲得的好處和付出的代價是如此地不成比例。

2013年9月19日 星期四

是否能讓 C++ template 的標頭檔只含宣告不含實作?

參考《C++ Templates - The Complete Guide》 ch 6.1 ~ 6.3, 答案是: 可以。

過去一直覺得自訂 template 的時候, 將宣告和定義 (實作) 同時放在標頭檔裡最保險, 但不確定是否能將兩者拆開放到不同檔案。拆開的明顯好處是不會因為修改 template 的實作, 而需要重新編譯 include 此標頭檔的檔案。在經歷過改一行標頭檔要重新編譯十分鐘後, 我愈來愈在意這件事了。

在說明如何折開 template 的宣告和實作之前, 得先明白編譯使用到 template 程式碼過程發生了什麼事。實際上有兩個步驟需要留意:

  • 讀入 template 宣告, 檢查 caller 是否有正確使用目標函式、類別。
  • instantiate (實例化) 特定參數的 template

比方說定義 std::map<std::string, int> scores 的時候, 除了需要 map 的宣告了解 scores 是否有正確使用 map 的介面外, 還需要 map 的定義 (實作)才知道如何 instantiate std::map<std::string, int>。實例化時會檢查參數 std::string 和 int 是否支援 map 預期的介面。附帶一提, 《Effective C++》稱 template 為「隱式介面 + 編譯期多型」, 而 virtual 是「顯示介面 + 執行期多型」, 很精闢的描述。

回到原本的議題, template 標頭檔是否能只含宣告? 可以, 只要之後有辦法 instantiate 用到的 template 即可。假設 <map> 裡只有 std::map 的宣告, 就需要在某個 cpp 檔裡面 include map 的定義, 然後明確地告訴 compiler 你要 instantiate std::map<std::string, int>

以下以自訂函式說明:

t.h

#ifndef T_H
#define T_H

template <typename T>
void foo(T& t);

#endif //  T_H

t.cpp

#include "t.h"
#include <vector>

template <typename T>
void foo(T&t) { t[0] = 10; }

template void foo<std::vector<int> >(std::vector<int>& t); // *注意*

main.cpp

#include <iostream>
#include <vector>
#include "t.h"

int main(void) {
  std::vector<int> ns;
  ns.push_back(0);
  std::cout << ns[0] << std::endl;
  foo(ns);
  std::cout << ns[0] << std::endl;
  return 0;
}

編譯和執行:

$ g++ -c t.cpp
$ g++ -c main.cpp
$ g++ t.o main.o -o main
$ ./main
0
10

注意 t.cpp 的最後一行, 這行就是使用 explicit instantiation 的語法, 要求編譯器 instantiate 指定參數的 template。

反之, 若少了那一行:

$ g++ -c t.cpp
$ g++ -c main.cpp
$ g++ t.o main.o -o main
main.o: In function `main':
main.cpp:(.text+0x63): undefined reference to `void foo<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >&)'
collect2: ld returned 1 exit status

編譯各別 cpp 檔時沒有問題, 但是 link object 檔的時候會回報找不到實例化的函式 foo(), 因為編譯 t.cpp 時, 編譯器不知道需要實例化帶有什麼參數的 foo, t.o 裡也就沒有 main.cpp 用到的 foo 了。

雖然看似 explicit instantiation 可以幫忙拆離 template 的宣告和定義, 《C++ Templates - The Complete Guide》 卻不建議這麼做, 原因是專案變大後很可能會漏掉需要 instantiate 的型別。

說了這麼多, 最後又說建議不使用。那 explicit instantiation 到底有什麼用? 參考 Minimizing Code Bloat: Redundant Template Instantiation 得知, 若有某個型別的 template 很常被使用 (如 std::string), 可以用它來減少編譯和連結的時間。作法是用 explicit template instantiation declarations 避免程式碼 instantiate template, 然後在某個地方自己明確地使用一次 explicit instantiation。

以下以 <string> 為例, 尋找用到此技巧的相關資訊。

/usr/include/c++/4.6/string.h 會 include <bits/stringfwd.h> 和 <bits/basic_string.tcc>。前者宣告 string 如下:

typedef basic_string<char>    string;   /// A string of @c char

後者讓 include 此標頭檔的檔案不會 instantiate string:

extern template class basic_string<char>;

最後, 在 /usr/lib/debug/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.16 裡可以找到 string 的實體 (應該是啦, 我沒有很仔細地比對):

$ nm /usr/lib/debug/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.16 | g _ZStrsIcSt11char_traitsIcESaIcEERSt13basic_istreamIT_T0_ES7_RSbIS4_S5_T1_E
000000000006b810 T _ZStrsIcSt11char_traitsIcESaIcEERSt13basic_istreamIT_T0_ES7_RSbIS4_S5_T1_E
$ c++filt _ZStrsIcSt11char_traitsIcESaIcEERSt13basic_istreamIT_T0_ES7_RSbIS4_S5_T1_E
std::basic_istream<char, std::char_traits<char> >& std::operator>><char, std::char_traits<char>, std::allocator<char> >(std::basic_istream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<char>, std::allocator<char> >&)

或在 gcc-4.6.3/libstdc++-v3/src/string-inst.cc 中找到這段:

#include <string>

// Instantiation configuration.
#ifndef C
# define C char
#endif

namespace std _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION

  typedef basic_string<C> S;

  template class basic_string<C>;

2013年9月18日 星期三

python 快速開發: 使用 ipython 撰寫 python 程式

一但習慣某個東西的好處後, 久了就會忘了它的重要性。今天重操舊業寫了久違的 python, 覺得能用 python 寫程式已很爽了, 還能用 ipython 寫 python, 更是爽上加爽!特此記錄一下, 分享給還未試過的人。

ipython 是 python 的互動式 shell, 功能相當強大, 而且預設設定就相當好用。如今 ipython 已發展到令人難以想像的地步, 以下只是基本的使用情境。

試用別人的模組

$ ipython
Python 2.7.3 (default, Aug  1 2012, 05:14:39)
Type "copyright", "credits" or "license" for more information.

IPython 0.13.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: import os

In [2]: os.pa<TAB>
os.pardir          os.pathconf        os.pathsep
os.path            os.pathconf_names

In [2]: os.path.join?
Type:       function
String Form:<function join at 0x7f67f9c09ed8>
File:       /usr/lib/python2.7/posixpath.py
Definition: os.path.join(a, *p)
Docstring:
Join two or more pathname components, inserting '/' as needed.
If any component is an absolute path, all previous path components
will be discarded.

In [3]: os.path.join('a', 'b')
Out[3]: 'a/b'

In [4]: os.path.join??
Type:       function
String Form:<function join at 0x7f67f9c09ed8>
File:       /usr/lib/python2.7/posixpath.py
Definition: os.path.join(a, *p)
Source:
def join(a, *p):
    """Join two or more pathname components, inserting '/' as needed.
    If any component is an absolute path, all previous path components
    will be discarded."""
    path = a
    for b in p:
        if b.startswith('/'):
            path = b
        elif path == '' or path.endswith('/'):
            path +=  b
        else:
            path += '/' + b
    return path

首先我先 import os, 第二行打了 os.pa<TAB> 看看有那些名稱開頭為 "pa" 的子物件或函式可用。也可以用 os.<TAB> 看 os 下的全部物件和函式。

接著找到 os.path.join, 在它後面加上 "?", 表示想知道 os.path.join 的 python doc。了解用法後就在第三行試試, ipython 預設會輸出 expression 回傳的結果 (而且是用 pretty-print 的方式呈現)。

最後第四行用 "??" 查詢 os.path.join 的實作, 有時沒有文件或看不懂文件的話, 直接看原始碼也滿方便的。另外讀別人程式發覺 import module 順序太複雜時, 也可以先用 ipython import 進來, 再用 "??" 看最後 import 到的是那一份實作。也可從裡面提到的檔名再用編輯器打開來看。

有了 <TAB>、? 和 ?? 後, 不太需要從外部找參考手冊, 直接在 ipython 裡試比較快。

開發片段小程式

試了一些片段小程式, 了解怎麼使用別人的模組後, 再來會想拼湊一些小程式。這時可用 edit 進入編輯器 (會選用環境變數 EDITOR 設的值):

In [1]: edit
IPython will make a temporary file named: /tmp/ipython_edit_bAOgl9.py
Editing... done. Executing edited code...
Out[1]: 'class Rect(object):\n    def __init__(self, x, y, w, h):\n        self.x = x\n        self.y = y\n        self.w = w\n        self.h = h\n'

In [2]: r = Rect(0, 0, 100, 50)

In [3]: r.w
Out[3]: 100

In [4]: r.area()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-83a71df0c49c> in <module>()
----> 1 r.area()

AttributeError: 'Rect' object has no attribute 'area'


In [5]: edit -p
IPython will make a temporary file named: /tmp/ipython_edit_stFwGi.py
Editing... done. Executing edited code...
Out[5]: 'class Rect(object):\n    def __init__(self, x, y, w, h):\n        self.x = x\n        self.y = y\n        self.w = w\n        self.h = h\n\n    def area(self):\n        return self.w * self.h\n'

In [6]: r2 = Rect(0, 0, 100, 50)

In [7]: r2.area()
Out[7]: 5000

在第一行按 edit 後, 畫面會切到編輯器, 寫好內容存檔離開後, ipython 會載入剛才打的內容到 ipython 裡, 也就是第一行輸出 'class Rect(object):\n def __init__ ...' 那一串。這裡我定義了 class Rect, 然後試一下 Rect 看看有沒有寫錯。接著第五行 edit -p 表示回去編輯上一次的內容, 執行後一樣會進入編輯器, 不過內容會先有上一次打過的東西。這回我多補了 area() 函式, 於是新增的 Rect 物件就有這函式了。

拼湊差不多後, 可用 edit -p 取得內容, 另外存到檔案, 或是打 hist 看打過的內容, 手動剪貼到檔案裡。

開發完整程式

想法明確時, 直接用編輯器編寫, 再輔以 ipython 測試更有效率。

首先, 終端機A的編輯器畫面如下所示, 寫了一個階乘函數:

# foo.py
def factorial(n):
    s = 1
    for i in range(n):
        s *= i
    return s

然後在終端機B的 ipython 測試:

In [1]: import foo

In [2]: foo.factorial(3)
Out[2]: 0

In [3]: reload(foo)
Out[3]: <module 'foo' from 'foo.py'>

In [4]: foo.factorial(3)
Out[4]: 6

第二行測完發覺結果不對, 於是回終端機A修改, 將 range(n) 改為 range(1, n + 1), 再回來終端機B執行 reload(foo) 重新載入 foo。接著第四行重測就得到正確的結果了。

當然, 發覺結果不對時, 也可以在 ipython 測測 range() 的用法, 或用 range? 看看說明, 很快就會找到答案。

結語

以上只是基本的 ipython 用途, 相信開發速度已可以比單用編輯器寫 python 快上兩倍以上。強烈建議有寫 python 的人一定要用看看 ipython。

C++ 能否用 memcpy 複製 class / struct 的資料?

答案是: POD (plain old data) type 可以。POD type 可和 C 互通, CPP Reference POD Type 的介紹: Specifies that the type is POD (Plain Old Data) type. Thi...